javascript
数字孪生可视化开发技术(ThingJS)学习笔记
【文章简介】:笔者参加了一个由学堂在线和优锘科技(以下简称该公司)举办的数字孪生可视化开发技术训练营培训,该公司研发了低代码 3D可视化开发平台 ThingJS 相关生态链,主要用于以一种低代码简单的方式构建城市、园区等生产生活应用场景下数据展示。这些相关培训的内容也是本文的记录点。
注:本文仍在编辑,由于包含大量动态图,文章加载很慢,如无显示可以刷新试试 (编辑时间:202208-202209)
jcLee95 的个人博客
邮箱 :291148484@163.com
CSDN 主页:https://blog.csdn.net/qq_28550263?spm=1001.2101.3001.5343
本文地址:https://blog.csdn.net/qq_28550263/article/details/126156985
目 录
1. 数字孪生的相关概念
2. ThingJS生态体系
3. 搭建园区
- 3.1 CampusBuilder 客户端的安装和启动
- 3.2 CampusBuilder 的在线使用
- 3.3 实战环节
- 3.3.1 室内场景搭建实例
- 3.3.2 室外与室内
4. CityBuilder 搭建城市
- 4.1 登录系统
- 4.2 添加图层
5. ChartBuilder 构建大屏
- 5.1 概述
- 5.2 拖拽现有大屏布局模板
- 5.3 自己搭建大屏布局
- 5.4 画布中的图层
- 5.4.1 介绍
- 5.4.2 小案例
- 5.5 森大屏资源
- 5.5.1 资源类型
- 5.5.2 资源操作
- 5.5.2.1 搜索资源
- 5.5.2.2 添加资源
- 5.5.2.3 删除资源
- 5.5.2.4 重命名资源
- 5.5.2.5 移动资源所在图层
- 5.5.2.6 对齐资源
- 5.5.2.7 打组资源
- 5.5.2.8 锁定资源
- 5.5.2.9 隐藏资源
- 5.5.2.10 复制资源
- 5.5.2.11 设置资源属性
- 5.5.2.12 管理资源样式
- 5.5.2.13 管理颜色方案
6. ThingJS API 使用
-
6.1 场景与园区
- 6.1.1 场景与园区的概念
- 6.1.2 如何创建场景
- 6.1.3 如何让导出的模型可拾取
- 6.1.4 如何加载多个园区
- 6.1.5 场景效果配置
- (1)设置背景
- (2)设置聚光灯
-
6.2 层级
- 6.2.1 层级获取
- 6.2.2 层级切换
- 6.2.3 层级事件
- (1)进入层级事件
- (2)层级改变事件
- (3)离开层级事件
- (4)层级飞行结束事件
- (5)进入层级场景响应事件
- (6)进入层级飞行响应事件
- (7)进入层级背景设置事件
- (8)默认层级拾取结果
- (9)退出层级场景响应事件
- (10)修改默认的拾取物体操作
- (11)修改进入层级操作
- (12)修改退出层级操作
- 6.2.4 展示场景层级示例
- 6.2.5 展示建筑外部结构示例
- 6.2.6 展示建筑内部结构示例
- 6.2.7 场景层级控制示例
-
6.3 对象控制
- 6.3.1 对象的增删查
- 6.3.1.1 创建对象的类型
- 6.3.1.2 创建和删除对象的 API
- 6.3.1.3 示例
- 6.3.2 对象效果设置
- 6.3.2.1 基础效果常用的 API
- 6.3.2.2 BaseStyle 类成员
- (1)设置物体是否始终在最前端渲染显示
- (2)显示/隐藏物体包围盒
- (3)设置包围盒颜色
- (4)设置/获取物体颜色
- (5)设置双面渲染
- (6)设置/获取材质自发光颜色
- (7)设置/获取材质自发光滚动贴图
- (8)设置/获取反射贴图
- (9)设置/获取高亮颜色
- (10)设置/获取高亮强度
- (11)设置贴图 填写图片资源路径 或 image 对象
- (12)材质金属度系数
- (13)设置/获取物体不透明度
- (14)设置/获取物体勾边颜色
- (15)设置/获取渲染排序值
- (16)设置材质粗糙度系数
- (17)开启/禁用勾边
- (18)开启/关闭线框模式
- 6.3.2.3 实例
- 6.3.1 对象的增删查
-
6.4 事件绑定
- 6.4.1 事件的全局绑定
- 6.4.2 事件的局部绑定
- 6.4.3 内核事件EventType 属性
- 6.4.4 事件的暂停和恢复
- 6.4.5 事件的卸载
- 6.4.6 自定义事件
- 6.4.7 实例
-
6.5 视角(摄影机)
- 6.5.1 摄像机的基本概念
- 6.5.2 ThingJS 中常用的 摄像机 API
- 6.5.2.1 官方案例解析 - 飞行控制
- 6.5.2.2 官方案例解析 - 控制交互
- 6.5.2.3 官方案例解析 - 控制地球相机
1. 数字孪生的相关概念
ThingJS 与传统3D开发的区别
| 人员配备 | 需招募并长期保有专业3D开发团队,成本高,管理难度大 | 现有开发团队可立即上手开发3D可视化应用,无需组建新团队 |
| 开发效率 | 基于底层引擎开发,开发效率低,升级、维护难度大 | ThingJS比传统3D开发提升10倍以上开发效率,维护简单 |
| 3D场景制作 | 需招募或外包3D场景制作,成本高,交付时间长,不易修改 | 基于CampusBuilder 和 CityBuilder,非专业人员即可快速生成并修改3D场景 |
| 3D模型制作 | 需专业3D建模人员制作模型,增加成本和交付时间 | ThingDepot 为物联网管理场景提供包括上万种3D模型的模型库 |
| 系统部署 | 安装调试复杂,部署和维护的成本很高 | 提供共有云服务和私有云部署,开发完成后立即运行,降低成本,提升交付效率 |
2. ThingJS生态体系
| CampusBuilder | 3D 园区搭建工具:可快速搭建园区级别 3D 可视化场景。 |
| CityBuilder | 3D 城市搭建工具:可快速搭建城市级别 3D 可视化场景。 |
| ChartBuilder | 图表制作工具:用于项目数据可视化。 |
| ThingJS 在线开发 | 了解 ThingJS 在线开发平台的界面和使用方法。 |
| ThingJS 离线开发网络版 | 了解 ThingJS 离线开发网络版的使用方法。 |
| ThingJS API | 强大的应用编程接口,轻松开发您的可视化项目应用。 |
3. 搭建园区
CampusBuilder(模模搭)是ThingJS体系内的3D园区场景搭建的客户端工具,如果你不想安装客户端,也可以在线使用 森园区(相当于 CampusBuilder 的网页版)搭建你的园区。在本文中,两种方式我们都会进行介绍。以下是我们本节实战小节搭建的效果:
3.1 CampusBuilder 客户端的安装和启动
登录完成后,你将看到如下界面:
3.2 CampusBuilder 的在线使用入口
你也可以不下载客户端而直接使用在线开发方式。在浏览器中进入 森园区 页面:https://studio.thingjs.com/campus,点击新建园区,如图所示:
页面将打开一个网页版的园区构建工具:
3.3 实战环节
这里我们随便在某度搜索一张室内设计为例,当然你也可以搜索一张产业园区的规划图纸,或者办公室的室内布局图纸:
以下是我选择的图片:
这章图片上有清晰的尺度标识,对于我们后续设定比例尺的工作是有帮助的。
接着我们以在线编辑为例进行讲解。
我们先新建园区:
3.3.1 室内场景搭建实例
点击“参考图”,导入我们的底图:
新建园区后,我们在森园区中,首先我们需要加载该图片,作为后续放置物件的蓝图:
图片导入后,调整比例尺的两端,到已知实际长度的位置,并在中间的实际大小中输入长度值,构建绘图的真实比例尺。
最后点击 “完成”,完成后布局图片将默认水平铺在地面上:
这时你可以从左侧“模型库”中选择相应的模型。比如选择 **“室内”**中的 “墙”:
你可可以删掉默认的小人,在**“生物”->“人”**中选择人物,放置在某个位置:
对于放置好的强,是可以拖动位置,以及拖动长度的:
在 公共库的 “室内” -> “家具”,找到椅子,放置在图纸上,通过拖到和旋转的方式调整位置、角度、大小:
以上都是直接放置在地面的,吊灯、电视都是相对于悬挂在空中的。这个需要调整 竖直方向上的高度。先简单放置一个吊灯:
使用鼠标拖动**“上下位移”**按钮,向上拖动:
直到达到你需要的高度:
你可以在右上角的切换按钮切换3D和2D视图:
可想而知,如果你的3D园区完成了,比平面布局更美观的俯视3D场景的2D图也就有了。
接着,完成门窗、电器,以及其它的内容,直到完成。最后就是本节开头时我们展现的图片:
3.3.2 室外与室内
到此为止,我们还只是介绍了园区的搭建,这仅仅是3D场景一部分,还不算数字孪生。
4. CityBuilder 搭建城市
森城市CityBuilder面向城市复杂场景的可视化需求,内置了全国范围内110多个城市的标准3D场景和酷炫的效果模板,使您分钟级构建心仪的3D城市。同时,森城市提供多种城市数据的插入和编辑能力,轻松让您的城市数据3D起来,实现整个城市的数字化及可视化。
4.1 登录系统
1.登录ThingStudio 森工厂。
2.在ThingStudio页面上方选择城市。
3.在城市页面单击新建城市,单击后跳转到CityBuilder主界面,如下图。
4.2 添加图层
CityBuilder提供了标准的城市三维场景资源——“森城市”,方便用户快速创建城市三维场景;同时您也可以插入自己的城市场景数据,满足用户个性化的场景需求。
初次进入到CityBuilder页面,系统会提醒您立马插入个人数据或选择需要添加的森城市资源,此时添加图层数据后,一个城市三维场景也随即创建成功。CityBuilder为方便用户快速获取城市三维场景,提供一套覆盖全国的标准城市三维场景资源,您可以在系统中森城市资源里直接选择区域添加至我的图层里;
CityBuilder还支持添加用户本地或已上传的用户资源(矢量数据),目前我们可以且仅可以使用本地矢量数据添加图层。
CityBuilder还支持在城市场景里加载森园区中搭建的园区,并提供了对园区位置和属性的编辑功能;
CityBuilder为方便用户自由的进行矢量数据的编辑,支持矢量数据图层的新增,并提供相应“矢量图层”对象的新增、删除和属性编辑功能。
一键添加
你可以在“森城市”资源信息面板里直接选择区域添加至我的图层里,区域范围支持以行政区划、自定义范围-多边形、自定义范围-矩形和自定义-圆形四种方式来进行选择。在搜索框,搜索,并选择你的城市,如郴州市:
点击**“添加至我的图层”**,可以将当前选择范围的城市加载到 **“我的图层”**中:
5. ChartBuilder 构建大屏
5.1 概述
森大屏是一个拖拽组装数字孪生可视化大屏的软件工具,提供丰富模板库,让可视化大屏无需从零开始搭建;提供数据接入和处理功能,实时展现图表数据;同时可以将3D场景/拓扑拖入森大屏,实现图表等指标数据与三维场景/拓扑进行联动交互。
在 森工厂 中选择 大屏 或者直接在地址栏中输入地址 https://studio.thingjs.com/ui 可以进入森大屏主页:
点击 新建大屏 即可开启你的大屏构建:
5.2 拖拽现有大屏布局模板
进入 森大屏 后,在左侧面板 布局 下的 大屏模板 处可以看到有很多现成的大屏模板。我们可以选择其中的一个,使用鼠标左键点击并拖至右侧举行区域中松开,例如:
松开鼠标后,被托选的大屏将在这个举行区域中展示出来:
快捷键:
| 保存 | Ctrl+S | ⌘S |
| 打开 | Ctrl+O | ⌘O |
| 重命名 | Ctrl+Shift+R | ⇧⌘R |
| 撤销 | Ctrl+Z | ⌘Z |
| 恢复 | Ctrl+Y | ⌘Y |
| 复制 | Ctrl+C | ⌘C |
| 粘贴 | Ctrl+V | ⌘V |
| 左侧面板 | Ctrl+1 | ⌘1 |
| 右侧面板 | Ctrl+2 | ⌘2 |
| 大纲 | Ctrl+3 | ⌘3 |
| 放大 | Ctrl+= | ⌘+ |
| 缩小 | Ctrl± | ⌘- |
| 放缩至100% | Ctrl+0 | ⌘0 |
| 置顶 | Ctrl+Shift+] | ⌥⌘] |
| 置底 | Ctrl+Shift+[ | ⌥⌘[ |
| 上移一层 | Ctrl+] | ⌘] |
| 下移一层 | Ctrl+[ | ⌘[ |
| 左对齐 | Alt+A | ⌥A |
| 右对齐 | Alt+D | ⌥D |
| 顶对齐 | Alt+W | ⌥W |
| 底对齐 | Alt+S | ⌥S |
| 水平对齐 | Alt+H | ⌥H |
| 垂直对齐 | Alt+V | ⌥V |
| 锁定 | Ctrl+Shift+L | ⇧⌘L |
| 打组 | Ctrl+G | ⌘G |
| 解除打组 | Ctrl+Shift+G | ⇧⌘G |
| 运行 | F5 | F5 |
5.3 自己搭建大屏布局
图表资源的布局方法包含资源移动、资源缩放、资源对齐、资源打组、取消打组、资源锁定、资源隐藏、资源复制、资源升级、资源图层位置移动、资源删除等,您可以通过这些方法快捷地进行图表资源布局。
5.4 画布中的图层
5.4.1 介绍
画布中新建 多个图层 来展示不同的业务场景,并将主场景设置为 常显,设置为常显后主场景在每个图层均显示,方便您根据主场景来搭建不同的业务场景。
如图所示:
- 【新建图层】在画布左下方单击“”图标新建图层,新建的图层展示在已有图层后面;
- 【常显图层】选择要设置为常显的图层,单击“”图标打开图层的菜单,选择 常显 选项,即可将当前图层设置为 常显图层;
- 孪生资源拖入大屏后(大屏场景中仅支持拖入一个孪生资源),系统自动将展示资源的图层设置为3D图层,并将图层置为常显状态。
- 【图层顺序拖动】拖动图层名称可移动图层的前后位置;
- 【图层重命名】单击“”图标打开图层的菜单,选择 重命名 选项,可以对图层进行重命名;
- 【图层删除】单击图标“”打开图层的菜单,选择 删除 按钮即可删除图层;
- 【图层复制】针对需要复制的图层,单击图标“”打开图层的菜单,选择 复制 按钮对图层进行复制。单击菜单栏的 编辑>粘贴 或者使用快捷键 Ctrl+V 即可粘贴复制的图层。粘贴的图层展示在已有图层后面。
- 3D图层不支持复制操作
5.4.2 小案例
先拖入一个孪生体 3D 图层:
可以看到这个图层有一个 锁定的符号,即,表明它是 常显图层。我们希望这个图层显示在最下方,也就是最底层。在其上面显示各种数据面板。
于是我们从 布局 中的 大屏模板 中,选择一个模板,作为第二个图层,这个图层在孪生体图层的上面:
大屏模板 图层在上面,由于它有一张自带的背景图,这个图似乎是不透明的,将下面的图层给 遮挡 住了。因此我们需要将该大屏模板用到的 背景图删除或者设置为隐藏。
点击 大纲,打开该 大屏模板的资源目录:
将鼠标逐个移动到列表中的资源项上,右侧将显示对应资源的控制图标,点击 图中 圈出控制图标,可以让相应的资源进行隐藏/显示:
最终,我们隐藏了背景图,就是这样的效果了:
5.5 森大屏资源
5.5.1 资源类型
| 孪生 | 包含园区、城市、拓扑、低代码以及零代码孪生资源,我的页签展示您在森园区、森城市、森拓扑、低代码平台以及零代码平台搭建的孪生资源,官方页签展示官方提供的园区、城市、拓扑、低代码以及零代码孪生资源,默认展示您搭建的孪生资源。 |
| 布局 | 包含官方提供的多个大屏模板和布局模板。 |
| 图表 | 包含官方提供的柱状图、条形图、折线图、曲线图、面积图、饼环图、散点图、雷达图、关系图以及其他类型的图表资源;全部展示官方提供的全部图表资源。 |
| 文表 | 包含官方提供的通用标题、业务指标趋势、多行文本、状态卡片、时间器、跑马灯、倒计时、时间轴、进度条、键值表格、进度条表格、轮播列表柱状图以及轮播列表等文表资源。 |
| 控件 | 包含官方提供的iframe、轮播页面、图片、图标、形状、视频、音频、下拉框选择器、单选框、时间选择器、地理搜索框、Tab列表、输入框、全屏切换、开关以及按钮等控件资源。 |
| 素材 | 包含官方提供的形状、背景图以及背景框等素材资源。 |
| 其他 | 包含官方提供的接数组件资源。 |
| 主题 | 包含官方提供的主题图表资源以及第三方公司或个人提交的经过审核的主题图表资源,目前包含矩阵革命和极光主题。 |
| 我的 | 包含您定义开发并发版的图表、另存的图表模板以及导入的布局资源。详情请参见下方我的资源。 |
5.5.2 资源操作
5.5.2.1 搜索资源
5.5.2.2 添加资源
5.5.2.3 删除资源
5.5.2.4 重命名资源
5.5.2.5 移动资源所在图层
5.5.2.6 对齐资源
5.5.2.7 打组资源
5.5.2.8 锁定资源
5.5.2.9 隐藏资源
5.5.2.10 复制资源
5.5.2.11 设置资源属性
5.5.2.12 管理资源样式
5.5.2.13 管理颜色方案
颜色方案由色卡、背景色/图、文本、辅助色、网格线色等要素组成:
- 色卡:一个颜色方案可以定义多张色卡,系统内置了12套色卡,每张色卡由纯色、渐变色、图片组成,您可以选择系统内置的色卡使用,也可以自定义新建色卡。
- 背景色/图:图表的背景色或者背景图,开启后系统会将您选择的颜色或图片渲染到图表的背景框中,作为图表的背景色或者背景图。
- 文本:图表主体的文本颜色,默认为白色。
- 辅助色:图表主体的辅助色,辅助色在森大屏中用作保留色,主要用于不太起眼的点缀,烘托、支持和融合主色调,用于衬托图表主体的饱满性。
- 网格线色:图表主体的网格线色(例如列表中的表格边框色),图表主体中如果有线条作为重要组成部分,建议线条颜色的取色从颜色方案中获取,以便于整屏换色时更美观。
单击菜单栏的 视图>颜色方案管理,进入颜色方案管理页面:
在颜色方案管理页面单击新建颜色方案,弹出颜色方案设置弹框:
设置颜色方案色卡,色卡由纯色、渐变色和图片组成:
-
设置纯色:单击弹出颜色选择框,选择颜色后,单击颜色方案设置弹框中除颜色选择框外的其他位置可为色卡设置纯色。
-
设置渐变色:单击弹出颜色选择框,选择渐变色后,单击颜色方案设置弹框中除颜色选择框外的其他位置可为色卡设置渐变色,渐变色支持设置线性渐变和径向渐变,设置线型渐变时可设置线性渐变的角度。颜色选择框会自动记录您最近使用的16个颜色,当您需要使用同样的颜色时,可单击该颜色色块,将其应用到色卡上。
-
设置图片:单击弹出上传图片弹框,在弹框中单击上传/选择图片进入资源管理器页面,选择官方素材或者我的素材中的图片后,单击确认即可为色卡设置图片,设置图片时可在上传图片弹框右下角单击图标,设置图片的展示效果,支持选择拉伸、自适应和实际大小。
-
新增色卡:鼠标悬浮于数量后的色卡数字,单击图标可新增色卡。
-
删除色卡:单击色卡后的“
”图标可删除当前色卡,鼠标悬浮于数量后的色卡数字,单击图标可删除色卡列表中最下方的色卡。 -
排序色卡:单击色卡前的“
”图标拖动可调整当前色卡的顺序,单击色卡下方的“
”图标可将当前颜色方案中的色卡按相反顺序排列。 -
重命名色卡:单击页面上方的色卡名称,名称进入编辑模式,输入新的名称即可重命名色卡。
-
-
设置背景色/图:单击背景色/图前的图标开启设置功能:
6. ThingJS API 使用
本节的目标是熟练并掌握ThingJS一些常用的API,通过一些案例demo的穿插,加深大家对于ThingJS,开发在线项目或功能的能力。
6.1 场景与园区
6.1.1 场景与园区的概念
【场景】:当我们使用 App 启动了 ThingJS,ThingJS 就会创建一个三维空间,整个三维空间我们称之为 “场景”(scene),在场景内我们可以 创建对象,比如园区,建筑,等等。
在ThingJS中主要包括两类场景,一个是 园区场景,另外一个是 地球场景。
【园区】(campus):是一个对象。
6.1.2 如何创建场景
打开 ThingJS studio 官网https://studio.thingjs.com/lowCode进入其 低代码 模块,点击 新建 ThingJS 项目
浏览器将在新的标签中打开在线开发工具:
可以看到,新打开的这个 ThingJS 已经由如下代码:
前面说过,当我们使用 App 启动了 ThingJS,ThingJS 就会创建一个三维空间,启动的代码就是new THING.App这部分:
var app = new THING.App({url: 'https://www.thingjs.com/static/models/factory', // 场景地址background: '#000000',env: 'Seaside', });其中:
- url 指的是园区场景的一个地址,如这里为魔门提供的一个示例园区的地址:https://www.thingjs.com/static/models/factory。
- background是场景的背景色。
- env 是场景所处的一个虚拟环境,指定一个 env 在场景中存在如 镜子之类的,可以看到镜子中反射的周围环境的效果,如图 镜子反射了Seaside:
除了这几个属性外,还有其它的属性,详细可以参考其 API文档。
你可以使用自己的园区,需要点击这个按钮进行选取:
可以看到,我这里没有显示任何园区可以拾取。因此需要使用一个办法,让我们在 森园区 中搭建好的园区能够显示在这里的拾取区中。
6.1.3 如何让导出的模型可拾取
上面一节,我们知道,园区搭建好之后需要在 ThingJS 项目中拾取以使用它。那么如何才能让我们搭建好的园区可以拾取呢——只有在编辑了 UserID、Name 或者 自定义属性 后,导入到 ThingJS 中才能成为独立的管理对象,被程序读取或修改。
比如绘制的一个门,我们可以使用鼠标选中它,在右侧的编辑面板中给定它属性值,要让它可拾取,则指定其 孪生体ID,这里我们指定其 孪生体ID 和 名称 分别为 door_01 以及 door:
打开与效果预览,可以看到将鼠标移动到这个门上时能够显示黄色的线框,这就表明这扇门已经可以被拾取了。
在开发中只有一个对象可以被拾取,才表明它具有独立的身份,之所以叫数字孪生提是因为往往它对应着显示中的某个我们关注其某些具体参数的东西,是现实世界中的物体在数字世界的表示。如果不可拾取,则 ThingJS 会出于性能的考虑,将其认为是与环境融为一体。
6.1.4 如何加载多个园区
在之前的案例中,我们用过new THING.App({...}) 指定园区的 url 来创建园区:
var app = new THING.App({url: 'https://www.thingjs.com/static/models/factory', // 场景地址background: '#000000',env: 'Seaside', });实际上,这个例子中,我们可以先不指定园区的 url,在之后使用 app.create({}) 在其中给定创建类型为Campus(园区),并给出园区的 url,即:
var app = new THING.App({background: '#000000',env: 'Seaside', });app.create({type:'Campus',url: 'https://www.thingjs.com/static/models/factory',complete(ev){app.level.change(ev.object)} })同样的方式,当我们再次调用 create() 方法时,就可以创建第二个、第三个园区。如果创建了多个场景,那可以通过一个按钮(面板),来切换当前看到的场景,例如这个是一个官方的案例:
/*** 说明:通过动态加载场景 动态加载建筑里的楼层* 操作:双击建筑,动态加载场景*/var dataObj = { progress: 0 }; // 场景加载进度条数据对象 var loadingPanel; // 进度条界面组件 var curCampus;// 配置相应建筑的园区场景url var campusUrl = [{name: "园区A",url: "https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%8A%A8%E6%80%81%E5%B1%82%E7%BA%A7%E5%A4%96%E7%AB%8B%E9%9D%A2" }, {name: "园区B",url: "https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%9B%BE%E4%B9%A6%E9%A6%86%E5%A4%96" }]; var buildingConfig = {'商业A楼': 'https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%95%86%E4%B8%9AA%E6%A5%BC%E5%B1%82%E7%BA%A7','商业B楼': 'https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%95%86%E4%B8%9AB%E6%A5%BC%E5%B1%82%E7%BA%A7','商业C楼': 'https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%95%86%E4%B8%9AC%E6%A5%BC%E5%B1%82%E7%BA%A7','商业D楼': 'https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%95%86%E4%B8%9AD%E6%A5%BC%E5%B1%82%E7%BA%A7','商业E楼': 'https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%95%86%E4%B8%9AE%E6%A5%BC%E5%B1%82%E7%BA%A7','住宅A楼': 'https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E4%BD%8F%E5%AE%85%E6%A5%BC%E5%B1%82%E7%BA%A7','住宅B楼': 'https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E4%BD%8F%E5%AE%85%E6%A5%BC%E5%B1%82%E7%BA%A7','图书馆': 'https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%95%86%E4%B8%9AC%E6%A5%BC%E5%B1%82%E7%BA%A7', };var app = new THING.App({"url": "https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%8A%A8%E6%80%81%E5%B1%82%E7%BA%A7%E5%A4%96%E7%AB%8B%E9%9D%A2","skyBox": "Universal", });// 主场景加载完后 删掉楼层 app.on('load', function (ev) {curCampus = ev.campus;// 进入层级切换app.level.change(ev.campus);initThingJsTip("本例程通过动态创建场景,实现场景切换。场景切换后,双击进入建筑,可动态创建楼层。<br><br>当前位于:园区A");// 园区加载完成后,将园区中建筑下的楼层删除(Floor)for (var i = 0; i < ev.buildings.length; i++) {ev.buildings[i].floors.destroy();}new THING.widget.Button('切换场景', changeScene); // 切换场景createWidgets(); });/*** 切换场景*/ function changeScene() {var url = curCampus.url; // 当前园区url// 动态创建园区if (url === campusUrl[0].url) {createCampus(campusUrl[1]);} else {createCampus(campusUrl[0]);} }/*** 创建园区*/ function createCampus(obj) {app.create({type: "Campus",url: obj.url,position: [0, 0, 0],visible: false, // 创建园区过程中隐藏园区complete: function (ev) {initThingJsTip('本例程通过动态创建场景,实现场景切换。场景切换后,双击进入建筑,可动态创建楼层。<br><br>当前位于:' + obj.name);curCampus.destroy(); // 新园区创建完成后删除之前的curCampus = ev.object; // 将新园区赋给全局变量curCampus.fadeIn(); // 创建完成后显示(渐现)app.level.change(curCampus); // 开启层级切换var building = app.query(".Building"); // 获取园区中的建筑// 园区加载完成后,将园区中建筑下的楼层删除(Floor)for (var i = 0; i < building.length; i++) {building[i].floors.destroy();}}}); }/*** 卸载动态创建的园区*/ app.on(THING.EventType.LeaveLevel, '.Building', function (ev) {var current = ev.current;if (current.type == "Campus") {var building = ev.previous; // 获取之前的层级if (!building) return;building._isAlreadyBuildedFloors = false;if (building.floors) building.floors.destroy();var url = curCampus.url; // 当前园区urlif (url === campusUrl[0].url) {initThingJsTip('本例程通过动态创建场景,实现场景切换。场景切换后,双击进入建筑,可动态创建楼层。<br><br>当前位于:' + campusUrl[0].name);} else {initThingJsTip('本例程通过动态创建场景,实现场景切换。场景切换后,双击进入建筑,可动态创建楼层。<br><br>当前位于:' + campusUrl[1].name);}} }, '退出建筑时卸载建筑下的楼层');// 进入建筑时 动态加载园区 app.on(THING.EventType.EnterLevel, '.Building', function (ev) {var buildingMain = ev.object; // 获取当前建筑对象var buildingName = buildingMain.name; // 获取当前建筑名称var preObject = ev.previous; // 上一层级的物体// 如果是从楼层退出 进入Building的 则不做操作if (preObject instanceof THING.Floor) return;initThingJsTip(buildingName + '正在加载!');loadingPanel.visible = true;// 暂停进入建筑时的默认飞行操作,等待楼层创建完成app.pauseEvent(THING.EventType.EnterLevel, '.Building', THING.EventTag.LevelFly);// 暂停单击右键返回上一层级功能app.pauseEvent(THING.EventType.Click, '*', THING.EventTag.LevelBackOperation);// 动态创建园区var campusTmp = app.create({type: 'Campus',// 根据不同的建筑,传入园区相应的urlurl: buildingConfig[buildingName],// 在回调中,将动态创建的园区和园区下的建筑删除 只保留楼层 并添加到相应的建筑中complete: function () {var buildingTmp = campusTmp.buildings[0];buildingTmp.floors.forEach(function (floor) {buildingMain.add({object: floor,// 设置相对坐标,楼层相对于建筑的位置保持一致localPosition: floor.localPosition});})// 楼层添加后,删除园区以及内部的园区建筑buildingTmp.destroy();campusTmp.destroy();loadingPanel.visible = false;// 恢复默认的进入建筑飞行操作app.resumeEvent(THING.EventType.EnterLevel, '.Building', THING.EventTag.LevelFly);// 恢复单击右键返回上一层级功能app.resumeEvent(THING.EventType.Click, '*', THING.EventTag.LevelBackOperation);// 这一帧内 暂停自定义的 “进入建筑创建楼层” 响应app.pauseEventInFrame(THING.EventType.EnterLevel, '.Building', '进入建筑创建楼层');// 触发进入建筑的层级切换事件 从而触发内置响应buildingMain.trigger(THING.EventType.EnterLevel, ev);initThingJsTip(buildingName + '加载完成!');}}); }, '进入建筑创建楼层', 51);app.on(THING.EventType.LoadCampusProgress, function (ev) {var value = ev.progress;dataObj.progress = value; }, '加载场景进度');/*** 创建进度条组件*/ function createWidgets() {// 进度条界面组件loadingPanel = new THING.widget.Panel({titleText: '场景加载进度',opacity: 0.9, // 透明度hasTitle: true});// 设置进度条界面位置loadingPanel.positionOrigin = 'TR'// 基于界面右上角定位loadingPanel.position = ['100%', 0];loadingPanel.visible = false;loadingPanel.addNumberSlider(dataObj, 'progress').step(0.01).min(0).max(1).isPercentage(true); }其效果如下:
说明:
在这个案例中,主场景加载完后(使用app.on('load', (ev)=>{}))使用new THING.widget.Button,来创建了一个回调函数能够控制场景切换的按钮:
而这里,又用到了一个createCampus函数,这个函数是用来创建园区的:
function createCampus(obj) {app.create({type: "Campus",url: obj.url,position: [0, 0, 0],visible: false, // 创建园区过程中隐藏园区complete: function (ev) {initThingJsTip('本例程通过动态创建场景,实现场景切换。场景切换后,双击进入建筑,可动态创建楼层。<br><br>当前位于:' + obj.name);// curCampus 是一个该函数外面的全局变量,一开始时是用于容纳所有的园区的信息[{url:'xxx', name:'xxx'},...]// 主场景加载完后, curCampus 被赋值为 ev.campus;curCampus.destroy(); // 新园区创建完成后删除之前的园区curCampus = ev.object; // 将新园区赋给全局变量 curCampuscurCampus.fadeIn(); // 创建完成后显示(渐现)app.level.change(curCampus); // 开启层级切换// 获取园区中的建筑var building = app.query(".Building"); // 园区加载完成后,将园区中建筑下的楼层删除(Floor)for (var i = 0; i < building.length; i++) {building[i].floors.destroy();}}}); }该函数的唯一参数 obj 实际上就是用于指定园区的信息,包括url(园区地址)和name(用于展示的园区名称)两个属性,比如:
{name: "园区A",url: "https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%8A%A8%E6%80%81%E5%B1%82%E7%BA%A7%E5%A4%96%E7%AB%8B%E9%9D%A2" }又比如:
{name: "园区B",url: "https://www.thingjs.com/./uploads/wechat/oLX7p0wh7Ct3Y4sowypU5zinmUKY/scene/%E5%9B%BE%E4%B9%A6%E9%A6%86%E5%A4%96" }6.1.5 场景效果配置
场景效果配置,顾名思义,就是通过配置相关的参数,让场景看上去更符合我们的心意。
(1)设置背景
来看一个空过按钮控制背景的官方示例:
// 加载场景代码 var app = new THING.App({url: 'https://www.thingjs.com/static/models/factory', // 场景地址skyBox: 'Night',env: 'Seaside', });app.on('load', function () {initThingJsTip("天空盒是一个包裹整个场景的立方体,可以很好地渲染并展示整个场景环境。</br>点击左侧按钮,设置天空盒效果、背景色以及背景图片。");// 摄像机飞行到某位置app.camera.flyTo({'position': [14.929613003036518, 26.939904587373245, 67.14964454354718],'target': [2.1474740033704594, 17.384929223259824, 10.177959375514941],'time': 2000}); })// 设置天空盒(目前仅能使用系统内置天空盒效果) new THING.widget.Button('蓝天', function () {app.skyBox = 'BlueSky'; }); new THING.widget.Button('银河', function () {app.skyBox = 'MilkyWay'; }); new THING.widget.Button('黑夜', function () {app.skyBox = 'Night'; }); new THING.widget.Button('多云', function () {app.skyBox = 'CloudySky'; });new THING.widget.Button('灰白', function () {app.skyBox = 'White'; }); new THING.widget.Button('暗黑', function () {app.skyBox = 'Dark'; });// 背景色颜色可使用十六进制颜色或rgb字符串 new THING.widget.Button('设置背景色1', function () {app.background = '#0a3d62'; }) new THING.widget.Button('设置背景色2', function () {app.background = 'rgb(68,114,196)'; })// 图片可在资源、页面资源上传 // 上传完成后,点击需要使用的图片,即可在代码编辑器中出现图片url地址 // 也可直接使用能访问的网络图片url new THING.widget.Button('设置背景图片1', function () {app.background = 'https://www.thingjs.com/static/images/background_img_01.png'; }) new THING.widget.Button('设置背景图片2', function () {app.background = 'https://www.thingjs.com/static/images/background_img_02.png'; })// 清除背景效果 new THING.widget.Button('清除背景', function () {app.skyBox = null;app.background = null; })其效果如下:
(2)设置聚光灯
来看一个通过面板来设置聚光灯参数的官方示例:
// 加载场景代码 var app = new THING.App({url: 'https://www.thingjs.com/static/models/factory', // 场景地址skyBox: 'Night',env: 'Seaside', });// 参数 var dataObj = {'type': 'SpotLight','lightAngle': 30,'intensity': 1,'penumbra': 0.5,'castShadow': false,'position': null,'height': 0,'color': 0xFFFFFF,'distance': null,'target': null,'helper': true,'follow': true, }; // 叉车 let car1; let car2; // 当前灯光 let curLight; let curLightPosition; // 创建聚光灯方法 function createSpotLight(position, target) {dataObj['lightAngle'] = 30;dataObj['intensity'] = 0.5;dataObj['penumbra'] = 0.5;dataObj['castShadow'] = false;dataObj['position'] = position;dataObj['distance'] = 25;dataObj['color'] = 0xFFFFFF;dataObj['helper'] = true;dataObj['follow'] = true;//创建聚光灯var spotLight = app.create(dataObj);curLight = spotLight;curLightPosition = spotLight.position;createSpotLightControlPanel(spotLight);curLight.lookAt(car1); }/*** 灯光控制面板*/ function createSpotLightControlPanel() {var panel = new THING.widget.Panel({isDrag: true,titleText: "灯光参数调整",width: '260px',hasTitle: true});// 设置 panel 位置 panel.position = [10, 35];panel.addNumberSlider(dataObj, 'lightAngle').caption('灯光角度').step(1).min(0).max(180).isChangeValue(true).on('change', function(value) {curLight.lightAngle = value;});panel.addNumberSlider(dataObj, 'intensity').caption('亮度').step(0.01).min(0).max(1).isChangeValue(true).on('change', function(value) {curLight.intensity = value;});panel.addNumberSlider(dataObj, 'penumbra').caption('半影').step(0.01).min(0).max(1).isChangeValue(true).on('change', function(value) {curLight.penumbra = value;});panel.addNumberSlider(dataObj, 'distance').caption('距离').step(0.1).min(0).max(200).isChangeValue(true).on('change', function(value) {curLight.distance = value;});panel.addNumberSlider(dataObj, 'height').caption('高度').step(0.1).min(0).max(200).isChangeValue(true).on('change', function(value) {curLight.position = [curLightPosition[0], curLightPosition[1] + value, curLightPosition[2]];});panel.addBoolean(dataObj, 'castShadow').caption('影子').on('change', function(value) {curLight.castShadow = value;});panel.addBoolean(dataObj, 'helper').caption('辅助线').on('change', function(value) {curLight.helper = value;});panel.addBoolean(dataObj, 'follow').caption('跟随物体').on('change', function(value) {if (value) {curLight.lookAt(car1);} else {curLight.lookAt(null);}});panel.addColor(dataObj, 'color').caption('颜色').on('change', function(value) {curLight.lightColor = value;});}/*** 注册鼠标移动事件,检查是否按下'shift'键, 按下设置聚光灯跟随鼠标位置*/ app.on('mousemove', function(ev) {if (!curLight) {return;}if (!ev.shiftKey) {return;}var pickedPosition = ev.pickedPosition;if (pickedPosition) {curLight.lookAt(pickedPosition);} })/*** 注册场景load事件*/ app.on('load', function(ev) {// createTip();// 主灯强度设置为0,突出聚光灯效果app.lighting = {mainLight: {intensity: 0}};// 获取场景内id为'car01' 和 'car02' 的叉车car1 = app.query('car01')[0];car2 = app.query('car02')[0];// 参数1: 在car2上方5米创建一个聚光灯// 参数2: 初始target设置为car1的位置createSpotLight(THING.Math.addVector(car2.position, [0, 5, 0]), car1.position);// 创建一个圆形路径var path = [];var radius = 6;for (var degree = 0; degree <= 360; degree += 10) {var x = Math.cos(degree * 2 * Math.PI / 360) * radius;var z = Math.sin(degree * 2 * Math.PI / 360) * radius;path.push(THING.Math.addVector(car1.position, [x, 0, z]));}// 让 car1 沿圆形路径运动car1.movePath({orientToPath: true, // 物体移动时沿向路径方向path: path,time: 10 * 1000,loopType: THING.LoopType.Repeat // 循环类型});initThingJsTip("左侧面板可对灯光参数进行调整。按住 shift 键,聚光灯可追踪鼠标位置");$(".warninfo3").css("left", "55%"); })其效果如下:
6.2 层级
在 ThingJS 中的层级关系可以用一个树状结构来表示,即所谓 层级关系树:
这里一般来说主要关注 园区、 建筑、 楼层、 房间之间的关系,图片描述比较形象,这里不做过多解释。
| app.level | SceneLevel | 获取场景层次管理器 |
6.2.1 层级获取
| app.level.current | 获取当前的层级对象 |
| app.level.previos | 获取之前的层级对象 |
6.2.2 层级切换
| app.level.change(object) | 将层级切换到指定的对象 |
| app.level.back() | 返回当前层级的父物体层级 |
6.2.3 层级事件
(1)进入层级事件
// 进入层级 // {String} ev.level 当前层级标识枚举值 可通过 THING.LevelType 获取枚举值,如建筑层级标识为 THING.LevelType.Building // {THING.BaseObject} ev.object 当前层级对象(将要进入的层级对象) // {THING.BaseObject} ev.current 当前层级对象(将要进入的层级对象) // {THING.BaseObject} ev.previous 上一层级对象(离开的层级对象) app.on(THING.EventType.EnterLevel, '.Thing', function (ev) {var object = ev.object; });(2)层级改变事件
// 层级变化 // {String} ev.level 当前层级标识枚举值 可通过 THING.LevelType 获取枚举值,如建筑层级标识为 THING.LevelType.Building // {THING.BaseObject} ev.object 当前层级对象(将要进入的层级对象) // {THING.BaseObject} ev.current 当前层级对象(将要进入的层级对象) // {THING.BaseObject} ev.previous 上一层级对象(离开的层级对象) app.on(THING.EventType.LevelChange, function (ev) {var object = ev.current;if (object instanceof THING.Campus) {console.log('Campus: ' + object);}else if (object instanceof THING.Building) {console.log('Building: ' + object);}else if (object instanceof THING.Floor) {console.log('Floor: ' + object);}else if (object instanceof THING.Thing) {console.log('Thing: ' + object);} });(3)离开层级事件
// 离开层级 // {String} ev.level 当前层级标识枚举值 可通过 THING.LevelType 获取枚举值,如建筑层级标识为 THING.LevelType.Building // {THING.BaseObject} ev.object 当前层级对象(将要进入的层级对象) // {THING.BaseObject} ev.current 当前层级对象(将要进入的层级对象) // {THING.BaseObject} ev.previous 上一层级对象(离开的层级对象) app.on(THING.EventType.LeaveLevel, '.Thing', function (ev) {var object = ev.object; });(4)层级飞行结束事件
// 层级切换飞行结束 // {String} ev.level 当前层级标识枚举值 可通过 THING.LevelType 获取枚举值,如建筑层级标识为 THING.LevelType.Building // {THING.BaseObject} ev.object 当前层级对象(将要进入的层级对象) // {THING.BaseObject} ev.current 当前层级对象(将要进入的层级对象) // {THING.BaseObject} ev.previous 上一层级对象(离开的层级对象) app.on(THING.EventType.LevelFlyEnd, '.Thing', function (ev) {console.log(ev.object.id); });(5)进入层级场景响应事件
// 修改进入层级场景响应 // {String} ev.level 当前层级标识枚举值 可通过 THING.LevelType 获取枚举值,如建筑层级标识为 THING.LevelType.Building // {THING.BaseObject} ev.object 当前层级对象(将要进入的层级对象) // {THING.BaseObject} ev.current 当前层级对象(将要进入的层级对象) // {THING.BaseObject} ev.previous 上一层级对象(离开的层级对象) app.on(THING.EventType.EnterLevel, '.Thing', function (ev) {var object = ev.object;// 其他物体半透明var things = object.brothers.query('.Thing');things.style.opacity = 0.25;}, 'customEnterLevel');// 停止进入物体层级的默认行为 app.pauseEvent(THING.EventType.EnterLevel, '.Thing', THING.EventTag.LevelSceneOperations);(6)进入层级飞行响应事件
// 修改进入层级飞行响应 // {String} ev.level 当前层级标识枚举值 可通过 THING.LevelType 获取枚举值,如建筑层级标识为 THING.LevelType.Building // {THING.BaseObject} ev.object 当前层级对象(将要进入的层级对象) // {THING.BaseObject} ev.current 当前层级对象(将要进入的层级对象) // {THING.BaseObject} ev.previous 上一层级对象(离开的层级对象) app.on(THING.EventType.EnterLevel, '.Thing', function (ev) {var object = ev.object;app.camera.flyTo({object: object,xAngle: 45, //物体坐标系下沿x轴旋转角度yAngle: -45, //物体坐标系下沿y轴旋转角度radiusFactor: 2, //物体包围盒半径的倍数time: 3000,lerpType: THING.LerpType.Quartic.In,complete: function() {console.log("飞行结束");}});}, 'customLevelFly');// 停止进入物体层级的默认飞行行为 app.pauseEvent(THING.EventType.EnterLevel, '.Thing', THING.EventTag.LevelFly);(7)进入层级背景设置事件
// 修改进入层级背景设置 // {String} ev.level 当前层级标识枚举值 可通过 THING.LevelType 获取枚举值,如建筑层级标识为 THING.LevelType.Building // {THING.BaseObject} ev.object 当前层级对象(将要进入的层级对象) // {THING.BaseObject} ev.current 当前层级对象(将要进入的层级对象) // {THING.BaseObject} ev.previous 上一层级对象(离开的层级对象) app.on(THING.EventType.EnterLevel, '.Thing', function (ev) {app.skyBox = null;app.background = 0xffffff;}, 'customLevelSetBackground');// 停止进入物体层级的默认背景设置 app.pauseEvent(THING.EventType.EnterLevel, '.Thing', THING.EventTag.LevelSetBackground);(8)默认层级拾取结果
// 修改进入层级选择设置 // {String} ev.level 当前层级标识枚举值 可通过 THING.LevelType 获取枚举值,如建筑层级标识为 THING.LevelType.Building // {THING.BaseObject} ev.object 当前层级对象(将要进入的层级对象) // {THING.BaseObject} ev.current 当前层级对象(将要进入的层级对象) // {THING.BaseObject} ev.previous 上一层级对象(离开的层级对象) app.on(THING.EventType.EnterLevel, '.Building', function (ev) {app.picker.pickedResultFunc = function (obj) {return obj;} }, 'customLevelPickedResultFunc'); // 暂停建筑层级的默认选择行为 app.pauseEvent(THING.EventType.EnterLevel, '.Building', THING.EventTag.LevelPickedResultFunc);(9)退出层级场景响应事件
// 修改退出层级场景响应 // {String} ev.level 当前层级标识枚举值 可通过 THING.LevelType 获取枚举值,如建筑层级标识为 THING.LevelType.Building // {THING.BaseObject} ev.object 当前层级对象(将要进入的层级对象) // {THING.BaseObject} ev.current 当前层级对象(将要进入的层级对象) // {THING.BaseObject} ev.previous 上一层级对象(离开的层级对象) app.on(THING.EventType.LeaveLevel, '.Thing', function (ev) {var object = ev.object;// 取消其他物体半透明var things = object.brothers.query('.Thing');things.style.opacity = null;}, 'customLevelSceneOperations');// 暂停默认退出行为 app.pauseEvent(THING.EventType.LeaveLevel, '.Thing', THING.EventTag.LevelSceneOperations);(10)修改默认的拾取物体操作
// 修改默认的拾取物体操作// 鼠标拾取到物体变红 app.on(THING.EventType.MouseEnter, '.Thing', function(ev) {ev.object.style.color = '#FF0000'; }); // 鼠标离开物体取消变红 app.on(THING.EventType.MouseLeave, '.Thing', function(ev) {ev.object.style.color = null; });// 暂停默认的拾取物体操作 app.pauseEvent(THING.EventType.Pick, '*', THING.EventTag.LevelPickOperation);(11)修改进入层级操作
// 修改进入层级操作 // 单击进入 app.on(THING.EventType.SingleClick, function (ev) {var object = ev.object;if (object) {object.app.level.change(object);}}, 'customLevelEnterMethod');// 暂停双击进入 app.pauseEvent(THING.EventType.DBLClick, '*', THING.EventTag.LevelEnterOperation);(12)修改退出层级操作
app.on('load', function (ev) { // 场景加载完成后 进入园区层级 开启默认的层级控制app.level.change(ev.campus); });// 修改退出层级操作 // 双击右键回到上一层级 app.on(THING.EventType.DBLClick, function (ev) {if (ev.button != 2) {return;}app.level.back(); }, 'customLevelBackMethod');// 暂停单击返回上一层级功能 app.pauseEvent(THING.EventType.Click, null, THING.EventTag.LevelBackMethod)6.2.4 展示场景层级示例
下面来分析一个官方给出的展示场景层级示例:
// 引入jquery.easyui插件 THING.Utils.dynamicLoad(['/guide/lib/jquery.easyui.min.js', '/guide/lib/default/easyui.css'], function () {var panel =`<div class="easyui-panel" style="display:none;padding:5px; width: 220px;height: auto;margin-top: 10px;margin-left: 10px; position: absolute; top: 0px; right: 0; z-index: 1;background-color: white"><ul id="objectTree" class="easyui-tree"></ul></div>`$('#div2d').append($(panel)); })// 加载场景代码 var app = new THING.App({url: 'https://www.thingjs.com/static/models/factory', // 场景地址background: '#000000',env: 'Seaside', });// 定义父子树的显示状态 var objectTreeState = true; // 这里使用了jquery.easyui的tree插件 app.on('load', function (ev) {// 创建提示initThingJsTip("父子树:在 ThingJS 加载园区后,自动创建了由 campus,building,floor,room 和一些在模模搭中添加的Thing类物体。这些物体不是独立散落在场景中的,他们会相互关联,形成一棵树的结构。</br>点击左侧按钮,创建父子树,展示场景层级");app.camera.position = [45.620884740847416, 39.1713011011022, 57.12763372644285];app.camera.target = [1.7703319346792363, 4.877514886137977, -2.025030535593601];var buildings = app.query('.Building');// 创建父子树new THING.widget.Button('创建父子树', function () {// 提示内容修改initThingJsTip("点击右侧界面选择框,控制对应内容显隐");// 父子树界面创建以及控制$('#objectTree').parent().show();$('#objectTree').tree({data: getRootData(app.root),checkbox: true,cascadeCheck: false,onCheck: function (node, checked) {if (app.query('#' + node.id)[0]) {app.query('#' + node.id).visible = checked;if ((app.query('#' + node.id)[0].type) == "Campus") {changeBuilding(app.query('#' + node.id)[0].buildings);}if ((app.query('#' + node.id)[0].type) == "Building") {if (app.query('#' + node.id)[0].facades[0]) {app.query('#' + node.id)[0].floors.visible = false;}}} else {app.root.visible = checked;}},onClick: function (node, checked) {var id = node.id;var obj = app.query('#' + id)[0];if (obj) {app.level.change(obj);}}})});new THING.widget.Button('重置', function () {app.query("*").visible = true;app.query("*").style.opacity = 1;app.level.change(ev.campus);app.camera.position = [45.620884740847416, 39.1713011011022, 57.12763372644285];app.camera.target = [1.7703319346792363, 4.877514886137977, -2.025030535593601];buildings.forEach(function (item) {if (item.facades[0]) {item.floors.visible = false;}})$("#objectTree").html('');$(".easyui-panel").hide();initThingJsTip("父子树:在 ThingJS 加载园区后,自动创建了由 campus,building,floor,room 和一些在模模搭中添加的Thing类物体。这些物体不是独立散落在场景中的,他们会相互关联,形成一棵树的结构。</br>点击左侧按钮,创建父子树,展示场景层级");}) });/*** 根节点信息* @param {Object} root - root类*/ function getRootData(root) {var data = [];data.push(getSceneRoot(root));return data; }/*** 根节点信息* @param {Object} root - root类*/ function getSceneRoot(root) {var data = {id: root.id,checked: true,state: 'open',text: 'root',};data["children"] = [];root.campuses.forEach(function (campus) {data["children"].push(getCampusData(campus));});return data; }/*** 根节点信息由建筑和室外物体组成* @param {Object} campus - 园区类*/ function getCampusData(campus) {var data = {id: campus.id,checked: true,state: 'open',text: campus.type + ' (' + campus.id + ')'};data["children"] = [];campus.buildings.forEach(function (building) {data["children"].push(getBuildingData(building));});campus.things.forEach(function (thing) {data["children"].push(getThingData(thing));});return data; }/*** 收集建筑信息* @param {Object} building - 建筑对象*/ function getBuildingData(building) {var data = {id: building.id,checked: true,state: 'open',text: building.type + ' (' + building.id + ')'};data["children"] = [];building.floors.forEach(function (floor) {data["children"].push(getFloorData(floor));});return data; }/*** 收集楼层信息* @param {Object} floor - 楼层对象*/ function getFloorData(floor) {var data = {id: floor.id,checked: true,state: 'open',text: floor.type + ' (level:' + floor.levelNumber + ')'};data["children"] = [];floor.things.forEach(function (thing) {data["children"].push(getThingData(thing));});return data; }/*** 建筑对象* @param {Object} thing - 物对象*/ function getThingData(thing) {return {id: thing.id,checked: true,text: thing.type + ' (' + thing.name + ')'}; }/*** Building内部建筑隐藏(无外立面不隐藏内部建筑)* @param {Object} building - 建筑对象集合*/ function changeBuilding(building) {for (let i = 0; i < building.length; i++) {if (building[i].facades[0]) {building[i].floors.visible = false;}} }其效果如下:
6.2.5 展示建筑外部结构示例
下面来分析一个官方给出的展示建筑外部结构示例:
var campus;// 园区对象// 加载场景代码 var app = new THING.App({url: 'https://www.thingjs.com/static/models/factory', // 场景地址background: '#000000',env: 'Seaside',});app.on('load', function (ev) {// 创建提示initThingJsTip("点击按钮,可获取园区中的建筑(buildings)、物体(things)、地面(ground),设置建筑外立面显示隐藏");createHtml();campus = app.query(".Campus")[0]; // 获取园区对象new THING.widget.Button("获取buildings", function () {// 初始化设置reset();var buildings = campus.buildings; // 获取园区下的所有建筑,返回为 Selector 结构buildings.forEach(function (item) {// 创建标注var ui = app.create({type: 'UIAnchor',parent: item,element: createElement(item.id), // 此参数填写要添加的Dom元素localPosition: [0, 1, 0],pivot: [0.5, 1] //[0,0]即以界面左上角定位,[1,1]即以界面右下角进行定位});$('#' + item.id + ' .text').text(item.name);})})new THING.widget.Button("获取things", function () {// 初始化设置reset();// 获取园区下的所有 Thing 类物体,返回为 Selector 结构var things = campus.things;things.forEach(function (item) {// 创建标注var ui = app.create({type: 'UIAnchor',parent: item,element: createElement(item.id), // 此参数填写要添加的Dom元素localPosition: [0, 1, 0],pivot: [0.5, 1] //[0,0]即以界面左上角定位,[1,1]即以界面右下角进行定位});$('#' + item.id + ' .text').text(item.name);})})new THING.widget.Button("获取ground", function () {// 初始化设置reset()var ground = campus.ground; // 获取园区下的 ground// 创建标注var ui = app.create({type: 'UIAnchor',element: createElement(ground.id), // 此参数填写要添加的Dom元素position: [1.725, 0.02, 5.151],pivot: [0.5, 1] //[0,0]即以界面左上角定位,[1,1]即以界面右下角进行定位});$('#' + ground.id + ' .text').text('ground');})new THING.widget.Button("隐藏外立面", function () {// 初始化设置reset(true);var build = app.query('107')[0]; // 获取园区中的建筑if ($("input[value='隐藏外立面']").length) {$("input[value='隐藏外立面']").val('显示外立面');build.facade.visible = false; // 隐藏外立面build.floors.visible = true; // 显示楼层} else {$("input[value='显示外立面']").val('隐藏外立面');build.facade.visible = true; // 显示外立面build.floors.visible = false; // 隐藏楼层}})new THING.widget.Button("重置", function () {// 初始化设置reset();})/*** 恢复初始化*/function reset(flag) {$(".marker").remove(); // 移除标注if (flag) return;$("input[value='显示外立面']").val('隐藏外立面');var build = app.query('107')[0]; // 获取园区中的建筑build.facade.visible = true; // 显示外立面build.floors.visible = false; // 隐藏楼层createHtml();// 创建提示initThingJsTip("点击按钮,可获取园区中的建筑(buildings)、物体(things)、地面(ground),设置建筑外立面显示隐藏");}})/*** 创建html*/function createHtml() {var html =`<div id="board" class="marker" style="position: absolute;"><div class="text" style="color: #FF0000;font-size: 12px;text-shadow: white 0px 2px, white 2px 0px, white -2px 0px, white 0px -2px, white -1.4px -1.4px, white 1.4px 1.4px, white 1.4px -1.4px, white -1.4px 1.4px;margin-bottom: 5px;"></div><div class="picture" style="height: 30px;width: 30px;margin: auto;"><img src="/guide/examples/images/navigation/pointer.png" style="height: 100%;width: 100%;"></div></div>`;$('#div3d').append($(html));}/*** 创建元素*/function createElement(id) {var srcElem = document.getElementById('board');var newElem = srcElem.cloneNode(true);newElem.style.display = "block";newElem.setAttribute("id", id);app.domElement.insertBefore(newElem, srcElem);return newElem;}其效果如下:
6.2.6 展示建筑内部结构示例
下面来分析一个官方给出的展示建筑内部结构示例:
// 加载场景代码 var app = new THING.App({// 场景地址"url": "https://www.thingjs.com/./uploads/wechat/emhhbmd4aWFuZw==/scene/建筑测试03" });// 加载场景 app.on('load', function (ev) {var campus = ev.campus; // 获取园区对象var floor = app.query('.Floor')[0]; // 获取楼层对象app.level.change(floor); // 开启层级切换new THING.widget.Button('获取墙', getWall); // 获取楼层中的墙new THING.widget.Button('获取门', getDoor); // 获取楼层中的门new THING.widget.Button('获取 Thing', getThing); // 获取Thing类物体,包含门new THING.widget.Button('获取 Misc', getMisc); // 获取楼层中的misc类物体new THING.widget.Button('获取楼层地板', getFloor); // 获取楼层地板new THING.widget.Button('获取楼层屋顶', getFloorRoof); // 获取楼层屋顶 new THING.widget.Button('获取房间面积', getRoomArea); // 获取房间面积new THING.widget.Button('重置', init); // 恢复初始化设置$("input[type='button']").hide(); // 隐藏按钮 })/*** 获取当前楼层的Thing类物体*/ function getThing() {// 初始化设置init();initThingJsTip("搭建园区时,设置了 ID、name、自定义属性的模型,在 ThingJS 中均为 Thing 类物体");var floor = app.level.current; // 当前楼层var things = floor.things; // 楼层内Thing类物体things.forEach(function (item) {// 创建标注createUIAnchor('ui', item);}) }/*** 获取当前楼层中的门*/ function getDoor() {// 初始化设置init();initThingJsTip("获取楼层中的门。设置了 ID、name、自定义属性的门模型,才可以被获取");var floor = app.level.current; // 当前楼层var doors = floor.doors; // 楼层中的门doors.forEach(function (item) {// 创建标注createUIAnchor('ui', item);}) }/*** 获取当前楼层中的墙*/ function getWall() {// 初始化设置init();initThingJsTip("设置墙的颜色为黄色");var floor = app.level.current; // 当前楼层var wall = floor.wall; // 楼层中的墙wall.style.color = '#ffff00'; // 设置墙的颜色 }/*** 获取当前楼层misc类物体* 楼层下,只有在CampusBuilder中编辑了UserID、Name或自定义属性的物体(摆放的模型),* 才能在 ThingJS 中创建为 Thing 对象,否则将合并到楼层的 misc 中,无法单独进行管理。*/ function getMisc() {// 初始化设置init();initThingJsTip("园区搭建时,没有设置 ID、name、自定义属性的模型,都将合并到楼层的 Misc 中,无法单独进行管理");var floor = app.level.current; // 当前楼层var misc = floor.misc; // 楼层内misc类物体misc.style.outlineColor = '#0000ff'; // 设置misc类物体的颜色 }/*** 获取当前楼层的地板*/ function getFloor() {// 初始化设置init();initThingJsTip("楼层地板不包含本楼层下独立管理的房间地板");var floor = app.level.current; // 当前楼层var plan = floor.plan; // 楼层地板plan.style.color = '#ffff00'; // 设置地板颜色//添加标注createUIAnchor('text', plan, '楼层地板'); }/*** 获取当前楼层的屋顶*/ function getFloorRoof() {// 初始化设置init();initThingJsTip("楼层屋顶不包含本楼层下独立管理的房间屋顶");var floor = app.level.current; // 当前楼层var roof = floor.roof; // 楼层屋顶roof.style.opacity = 0.8; // 设置屋顶透明度roof.style.color = '#0000ff'; // 设置屋顶颜色roof.visible = true;//添加标注createUIAnchor('text', roof, '楼层屋顶'); }/*** 获取楼层内房间面积*/ function getRoomArea() {// 初始化设置init();var floor = app.level.current; // 当前楼层var textRegions = app.query('.TextRegion'); // 获取TextRegion类var rooms = floor.rooms; // 楼层的房间rooms.forEach(function (room) {room.roof.visible = true; // 显示房间屋顶room.roof.style.opacity = 0.8; // 设置透明度room.roof.style.color = '#0000ff'; // 设置颜色var area = room.area.toFixed(2); // 获取房间面积 保留小数点后两位//添加标注createUIAnchor('text', room, area + '平方米');initThingJsTip("展示房间面积");}) }/*** 初始化设置*/ function init() {var floor = app.level.current; // 当前楼层floor.wall.style.color = null; // 设置墙体颜色floor.misc.style.outlineColor = null; // 设置misc类物体颜色floor.plan.style.color = null; // 设置楼层地板颜色floor.roof.style.color = null; // 设置楼层屋顶颜色floor.roof.visible = false; // 设置楼层屋顶隐藏floor.rooms.forEach(function (room) {room.roof.visible = false; // 设置楼层房间隐藏room.roof.style.color = null; // 设置楼层房间屋顶颜色room.plan.style.color = null; // 设置楼层房间地板颜色})app.query('.TextRegion').destroyAll(); // 获取TextRegion类$(".marker").remove(); // 移除标注// 创建元素createHtml();initThingJsTip("点击左侧按钮,查看具体效果"); }/*** 创建html*/ function createHtml() {var html =`<div id="board" class="marker" style="position: absolute;"><div class="text" style="color: #FF0000;font-size: 12px;text-shadow: white 0px 2px, white 2px 0px, white -2px 0px, white 0px -2px, white -1.4px -1.4px, white 1.4px 1.4px, white 1.4px -1.4px, white -1.4px 1.4px;margin-bottom: 5px;"></div><div class="picture" style="height: 30px;width: 30px;margin: auto;"><img src="/guide/examples/images/navigation/pointer.png" style="height: 100%;width: 100%;"></div></div>`;$('#div3d').append($(html)); }/*** 创建元素*/ function createElement(id) {var srcElem = document.getElementById('board');var newElem = srcElem.cloneNode(true);newElem.style.display = "block";newElem.setAttribute("id", id);app.domElement.insertBefore(newElem, srcElem);return newElem; }/*** 生成一个新面板*/ function createUIAnchor(type, obj, value) {if (type == 'ui') {// 创建UIAnchorvar ui = app.create({type: 'UIAnchor',parent: obj,element: createElement(obj.id), // 此参数填写要添加的Dom元素localPosition: [0, 1, 0],pivot: [0.5, 1] //[0,0]即以界面左上角定位,[1,1]即以界面右下角进行定位});if (!value) value = obj.name;$('#' + obj.id + ' .text').text(value);} else if (type == 'text') {// 创建文本var areaTxt = app.create({type: 'TextRegion',id: 'areaTxt_' + obj.id,parent: obj,localPosition: [0, 3.8, 0],text: value,inheritStyle: false,style: {fontColor: '#ff0000',fontSize: 20, // 文本字号大小}});areaTxt.rotateX(-90); // 旋转文本} }// 监听进入楼层事件 app.on(THING.EventType.EnterLevel, '.Floor', function (ev) {init();if (ev.current.name == '办公楼一层') {$("input[type='button']").show();} else {$("input[type='button']").hide();} }, '进入楼层显示面板')// 监听退出楼层事件 app.on(THING.EventType.LeaveLevel, '.Floor', function (ev) {init();$("input[type='button']").hide(); }, '退出楼层隐藏面板')其效果如下:
6.2.7 场景层级控制示例
下面来分析一个官方给出的场景层级控制示例:
/*** 说明:自定义层级切换效果* 功能:* 1.进入建筑层级摊开楼层* 2.进入楼层层级更换背景图* 3.双击物体,播放模型动画* 操作:点击按钮*/ // 加载场景代码 var app = new THING.App({url: 'https://www.thingjs.com/static/models/factory', // 场景地址background: '#000000',skyBox: 'Night',env: 'Seaside', });// 初始化完成后开启场景层级 var campus; app.on('load', function (ev) {campus = ev.campus;// 将层级切换到园区 开启场景层级app.level.change(ev.campus);initThingJsTip("本例程修改了原有进入层级的默认响应,自定义了新的层级响应。点击按钮,查看效果");new THING.widget.Button('修改层级飞行响应', setEnterFly);new THING.widget.Button('修改层级场景响应', setEnterLevel);new THING.widget.Button('修改层级背景', setEnterBack);new THING.widget.Button('重置', reset); });/*** 修改默认的层级飞行响应* 双击进入建筑层级,展开楼层* 退出建筑关闭摊开的楼层*/ function setEnterFly() {// 重置reset();initThingJsTip("修改默认进入层级飞行响应,双击进入建筑层级,展开楼层");// 暂停默认退出园区行为app.pauseEvent(THING.EventType.LeaveLevel, '.Campus', THING.EventTag.LevelSceneOperations);// 进入建筑摊开楼层app.on(THING.EventType.EnterLevel, '.Building', function (ev) {var previous = ev.previous; // 上一层级ev.current.expandFloors({'time': 1000,'complete': function () {console.log('ExpandFloor complete ');}});}, 'customEnterBuildingOperations');// 进入建筑保留天空盒app.pauseEvent(THING.EventType.EnterLevel, '.Building', THING.EventTag.LevelSetBackground);// 退出建筑关闭摊开的楼层app.on(THING.EventType.LeaveLevel, '.Building', function (ev) {var current = ev.current; // 当前层级ev.object.unexpandFloors({'time': 500,'complete': function () {console.log('Unexpand complete ');}});}, 'customLeaveBuildingOperations'); }/*** 修改进入层级场景响应* @property {Object} ev 进入物体层级的辅助数据* @property {THING.BaseObject} ev.object 当前层级* @property {THING.BaseObject} ev.current 当前层级* @property {THING.BaseObject} ev.previous 上一层级*/ function setEnterLevel() {// 重置reset();initThingJsTip("修改默认进入层级场景响应,双击飞到物体,其他物体渐隐(若物体存在动画,则播放动画)");// 修改进入层级场景响应app.on(THING.EventType.EnterLevel, '.Thing', function (ev) {var object = ev.object;// 其他物体渐隐var things = object.brothers.query('.Thing');things.fadeOut();// 尝试播放动画if (object.animationNames.length) {object.playAnimation({name: object.animationNames[0],});}}, 'customEnterThingOperations');// 停止进入物体层级的默认行为app.pauseEvent(THING.EventType.EnterLevel, '.Thing', THING.EventTag.LevelSceneOperations);// 修改退出层级场景响应app.on(THING.EventType.LeaveLevel, '.Thing', function (ev) {var object = ev.object;// 其他物体渐现var things = object.brothers.query('.Thing');things.fadeIn();// 反播动画if (object.animationNames.length) {object.playAnimation({name: object.animationNames[0],reverse: true});}}, 'customLeaveThingOperations'); }/*** 进入楼层设置背景* 进入楼层层级,修改背景*/ function setEnterBack() {// 重置reset();initThingJsTip("修改默认进入层级背景,进入楼层层级,修改背景");// 进入楼层设置背景app.on(THING.EventType.EnterLevel, '.Floor', function (ev) {var previous = ev.previous; // 上一层级// 从建筑进入楼层时if (previous instanceof THING.Building) {app.background = '/uploads/wechat/emhhbmd4aWFuZw==/file/img/bg_grid.png';}}, 'setFloorBackground');// 停止进入楼层层级的默认行为app.pauseEvent(THING.EventType.EnterLevel, '.Floor', THING.EventTag.LevelSetBackground);// 退出楼层设置背景app.on(THING.EventType.LeaveLevel, '.Floor', function (ev) {var current = ev.current; // 当前层级// 从楼层退出到建筑时if (current instanceof THING.Building) {app.background = null;app.skyBox = "Night";}}, 'customLeaveFloorOperations'); }/*** 重置* app.resumeEvent 暂停事件* app.off 卸载事件*/ function reset() {// 创建提示initThingJsTip('本例程修改了原有进入层级的默认响应,自定义了新的层级响应。点击按钮,查看效果');app.resumeEvent(THING.EventType.LeaveLevel, '.Campus', THING.EventTag.LevelSceneOperations);app.resumeEvent(THING.EventType.EnterLevel, '.Building', THING.EventTag.LevelSetBackground);app.resumeEvent(THING.EventType.EnterLevel, '.Floor', THING.EventTag.LevelSetBackground);app.resumeEvent(THING.EventType.EnterLevel, '.Thing', THING.EventTag.LevelSceneOperations);app.off(THING.EventType.EnterLevel, '.Building', 'customEnterBuildingOperations');app.off(THING.EventType.LeaveLevel, '.Building', 'customLeaveBuildingOperations');app.off(THING.EventType.EnterLevel, '.Floor', 'setFloorBackground');app.off(THING.EventType.LeaveLevel, '.Floor', 'customLeaveFloorOperations');app.off(THING.EventType.EnterLevel, '.Thing', 'customEnterThingOperations');app.off(THING.EventType.LeaveLevel, '.Thing', 'customLeaveThingOperations');var curLevel = app.level.current; // 当前层级app.skyBox = 'Night'; // 设置天空盒if (curLevel instanceof THING.Building) {curLevel.unexpandFloors({'time': 500,'complete': function () {console.log('Unexpand complete ');}});}app.level.change(campus); }其效果如下:
6.3 对象控制
本小节说的 对象 是指 场景 中的 某个物体。
6.3.1 对象的增删查
6.3.1.1 创建对象的类型
| Thing | 模型物体 |
| Box、Sphere、Plane、Cylinder、Tetrahedron | 基本形体 |
| Campus | 园区 |
| UIAnchor、Marker、WebView | 界面 |
| ParticleSystem | 粒子 |
| Line、RouteLine | 线 |
| Heatmap | 热力图 |
| 拓展 ThingJS 类 | 自定义类 |
6.3.1.2 创建和删除对象的 API
| app.create() | 创建对象 |
| obj.destory() | 删除对象 |
| obj.destoryAll() | 删除对象集合 |
6.3.1.3 示例
【例子】使用 create() 方法 创建一个 类型为 Box 的对象
// 加载场景代码 var app = new THING.App({ url: '/api/scene/a5bb1ed8259a2b023ae317d3' });// 默认创建一个长宽高为 1m ,轴心点在中心的正方体 var box= app.create({type:'Box',position: [0, 0, 0] //世界坐标系下的位置 }); // 创建正方体参数设置 var box = app.create({type: 'Box',width: 10,// 宽度height: 10,// 高度depth: 10,// 深度center: 'Bottom',// 轴心//widthSegments: 1,// 宽度上的节数//heightSegments: 1,// 高度上的节数//depthSegments: 1,// 深度上的节数position:[0,0,0]// 世界坐标系下的位置 });// 创建提示 initThingJsTip(`这是一个 Box 的示例`);6.3.2 对象效果设置
6.3.2.1 基础效果常用的 API
| obj.style.color = 'red' | 设置颜色 | 值为颜色名字符串或颜色值 |
| obj.style.color = null | 清除颜色 | |
| obj.style.opacity = 0.5 | 设置透明度 | 透明度的值可以是 0~1 |
| obj.style.outlineColor = 'green' | 设置勾边 | 值为颜色名字符串或颜色值 |
| obj.style.outlineColor = null | 取消勾边 | |
| obj.style.glow = true | 开启外发光 | |
| obj.style.innerGlow = true | 开启内发光 | 可与 color 属性搭配使用 |
| obj.style.image = '***.jpg' | 设置贴图 | |
| obj.scale = [2, 2, 2] | 设置缩放值 | |
| obj.size = [2, 2, 2] |
6.3.2.2 BaseStyle 类成员
BaseStyle 类是 ThingJS 提供的物体样式基类,物体对象中包含了style属性是该类的实例,可以通过改变style不同属性的属性值来实现对物体样式的控制。
(1)设置物体是否始终在最前端渲染显示
| alwaysOnTop | Boolean |
(2)显示/隐藏物体包围盒
| boundingBox | Boolean |
(3)设置包围盒颜色
| boundingBoxColor | Number | String |
(4)设置/获取物体颜色
| color | String|Number |
可填写 十六进制颜色值 或 rgb 字符串,取消颜色设置为 null
// 使用十六进制颜色 obj.style.color = '#ff0000'; // 使用 rgb 颜色 obj.style.color = 'rgb(255,0,0)'; // 取消颜色 obj.style.color = null;(5)设置双面渲染
| doubleSide | Boolean |
(6)设置/获取材质自发光颜色
| emissive | String | Number |
(7)设置/获取材质自发光滚动贴图
| emissiveScrollImage | String |
(8)设置/获取反射贴图
| environmentImage | String|Array |
(9)设置/获取高亮颜色
默认值为 null。
| highlight | String|Number |
(10)设置/获取高亮强度
默认为0.5。设置为null,则等效于恢复到0.5。 如果高亮颜色为null,则该属性没有实际效果。
| highlightIntensity | Number |
(11)设置贴图 填写图片资源路径 或 image 对象
| image | String | Object |
(12)材质金属度系数
| metalness | Number |
(13)设置/获取物体不透明度
0 为全透明,1为不透明。
| opacity | Number |
(14)设置/获取物体勾边颜色
| outlineColor | Number|String |
(15)设置/获取渲染排序值
数值越小越先渲染,默认值为 0
| renderOrder | Number |
(16)设置材质粗糙度系数
| roughness | Number |
(17)开启/禁用勾边
| skipOutline | Boolean |
(18)开启/关闭线框模式
| wireframe | Boolean |
6.3.2.3 实例
// 加载场景代码 var app = new THING.App({ url: '/api/scene/a5bb1ed8259a2b023ae317d3' });// 默认创建一个长宽高为 1m ,轴心点在中心的正方体 var box= app.create({type:'Box',position: [0, 0, 0] //世界坐标系下的位置 });// 创建正方体参数设置 var box = app.create({type: 'Box',width: 10,// 宽度height: 10,// 高度depth: 10,// 深度center: 'Bottom',// 轴心position:[0,0,0]// 世界坐标系下的位置 });// 先获取上面的盒子对象 var o = app.query(".Box");// 指定其颜色 o.style.color = "#F400EE"; // 指定其透明度 o.style.opacity = 0.6; // 开启线框模式 o.style.wireframe = true;其效果如下:
6.4 事件绑定
ThingJS 内置事件包含:鼠标点击、键盘输入、层级变化。其中 层级变化 相关的事件已经在 6.2.3 层级事件 小节中介绍过了。
6.4.1 事件的全局绑定
通过app.on可以绑定全局事件,例如:
// 不添加任何条件,鼠标点击即可触发 app.on("click", function(ev){console.log("clicked!"); })// 添加指定条件,这里表示只有 Thing 类型物体才会触发 app.on("click", ".Thing", function(ev){console.log(ev.object.id+" clicked!"); })// 添加多重条件,对建筑、楼层触发点击事件 app.on("click", ".Building || .Floor",function(ev){console.log("You clicked "+ev.object.id); })6.4.2 事件的局部绑定
事件的局部绑定针对一个 对象 或 Selector 集合,通过接口绑定:
// 当这个对象被点击,即可触发 obj.on("click", function(ev){console.log(ev.object.name); });// 添加特定条件,当这个对象为 marker 类型,或带有 marker 类型的子孙,即可触发 obj.on("click", ".Marker", function(ev){console.log(ev.object.name); });// 查询到 obj 下的所有 marker 物体(集合),注册 click 事件 obj.query(".Marker").on("click", function(ev){console.log(ev.object.name); })6.4.3 内核事件EventType 属性
| Complete | string | complete | 通知系统初始化完成 或 物体完成加载 |
| Resize | string | resize | 通知窗口大小变化(width, height) |
| Update | string | update | 通知每帧更新 |
| Progress | string | progress | 通知场景资源加载进度 |
| Load | string | load | 通知 App 初始化完成 或 场景、物体加载完成 |
| Unload | string | unload | 通知物体卸载 |
| Click | string | click | 通知鼠标点击,鼠标单击、双击均会触发 Click 事件(双击时候会触发两次) |
| DBLClick | string | dblclick | 通知鼠标双击 |
| SingleClick | string | singleclick | 通知鼠标单击(会有些许的延时,鼠标双击不会触发 SingleClick 单击事件) |
| MouseUp | string | mouseup | 通知鼠标键抬起 |
| MouseDown | string | mousedown | 通知鼠标键按下 |
| MouseMove | string | mousemove | 通知鼠标移动 |
| MouseWheel | string | mousewheel | 通知鼠标滚轮滚动 |
| MouseEnter | string | mouseenter | 通知鼠标首次移入物体 |
| MouseOver | string | mouseover | 通知鼠标首次移入物体, 会一直传递到父物体 |
| MouseLeave | string | mouseleave | 通知鼠标首次移出物体 |
| DragStart | string | dragstart | 通知物体拖拽开始 |
| Drag | string | drag | 通知物体拖拽进行中 |
| DragEnd | string | dragend | 通知物体拖拽结束 |
| KeyDown | string | keydown | 通知键盘按键按下 |
| KeyPress | string | keypress | 通知键盘按键一直被按下 |
| KeyUp | string | keyup | 通知键盘按键抬起 |
| CameraChangeStart | string | camerachangestart | 通知摄像机位置变动开始 |
| CameraChangeEnd | string | camerachangeend | 通知摄像机位置变动结束 |
| CameraChange | string | camerachange | 通知摄像机位置变动中 |
| CameraZoom | string | camerazoom | 摄像机向前/后滚动 |
| CameraViewChange | string | cameraviewchange | 通知摄像机观察模式改动 |
| Create | string | create | 通知物体创建完成 |
| Destroy | string | destroy | 通知物体删除完成 |
| Expand | string | expand | 通知建筑楼层被展开 |
| Unexpand | string | unexpand | 通知建筑楼层被合并 |
| Select | string | select | 通知物体被选择 |
| Deselect | string | deselect | 通知物体被取消选择 |
| SelectionChange | string | selectionchange | 通知物体选择集合更新 |
| LevelChange | string | levelchange | 通知场景层级发生改变 |
| EnterLevel | string | enterLevel | 通知进入下一层级 |
| LeaveLevel | string | leaveLevel | 通知退出当前层级 |
| LevelFlyEnd | string | levelflyend | 通知摄像机飞入下一层级结束 |
【Tip】:
在低代码开发界面,你可以打开开发者工具,将 JavaScript 上下文选择为 https://www.thingjs.com ,然后通过控制台的交互式输入:
如图:
可以看到能够查看到当前 EventType 的所有属性值:
{"Complete": "complete","Resize": "resize","Update": "update","Progress": "progress","Load": "load","Unload": "unload","Click": "click","DBLClick": "dblclick","SingleClick": "singleclick","MouseUp": "mouseup","MouseDown": "mousedown","MouseMove": "mousemove","MouseWheel": "mousewheel","MouseEnter": "mouseenter","MouseOver": "mouseover","MouseLeave": "mouseleave","DragStart": "dragstart","Drag": "drag","DragEnd": "dragend","KeyDown": "keydown","KeyPress": "keypress","KeyUp": "keyup","CameraChangeStart": "camerachangestart","CameraChangeEnd": "camerachangeend","CameraChange": "camerachange","CameraZoom": "camerazoom","CameraViewChange": "cameraviewchange","Create": "create","Destroy": "destroy","Expand": "expand","Unexpand": "unexpand","Select": "select","Deselect": "deselect","SelectionChange": "selectionchange","LevelChange": "levelchange","EnterLevel": "enterLevel","LeaveLevel": "leaveLevel","LevelFlyEnd": "levelflyend","AppComplete": "complete","LoadCampusProgress": "progress","LoadCampus": "load","UnloadCampus": "unload","PickObject": "pick","Dragging": "drag","CreateObject": "create","DestroyObject": "destroy","ExpandBuilding": "expand","UnexpandBuilding": "unexpand","SelectObject": "select","DeselectObject": "deselect","ObjectSelectionChanged": "selectionchange","PickedObjectChanged": "pickchange","BeforeLevelChange": "beforelevelchange","Pick": "pick","Unpick": "unpick","PickChange": "pickchange","AreaPickStart": "areapickstart","AreaPicking": "areapicking","AreaPickEnd": "areapickend","BeforeLoad": "beforeload" }6.4.4 事件的暂停和恢复
// 注册事件 app.on("click", ".Building", function(event){console.log("clicked!") },"tag1");// 暂停事件 app.pauseEvent("click", ".Building", "tag1");// 恢复事件 app.resumeEvent("click", ".Building", "tag1");卸载/暂停/恢复 事件时第二个参数必须传条件,如果没有条件,又需要传 tag,将条件传 null 。
6.4.5 事件的卸载
// 注册事件 app.on("click", function(event){console.log("clicked!") });// 卸载注册的事件 app.off("click");// 带 tag 的事件 app.on("click", ".Building", function(event){console.log("clicked!") });// 卸载所有 Building 的点击事件 app.off("click", ".Building");// 只卸载带有该 tag 名的 Building 的点击事件 app.off("click", ".Building", "tagName");6.4.6 自定义事件
const truck = app.query('thing01')[0];// 监听自定义事件 truck.on('customAlarm', (ev)=>{truck.style.color = 'red'; })// 触发自定义事件 trigger truck.trigger('customAlarm', {alarm: true})6.4.7 实例
6.5 视角(摄影机)
6.5.1 摄像机的基本概念
在 3D 开发中,摄像机指的是 用来确定观察 3D 场景的视角,它包含两个重要的位置参数:
- 镜头位置(position)
- 目标点(target)
6.5.2 ThingJS 中常用的 摄像机 API
| app.camera.position | 镜头位置 | |
| app.camera.target | 目标点 | 被拍摄物位置 |
| app.camera.lookAt() | “盯着”某一位置或物体看 | 可传参数:位置、对象、null |
| app.camera.fit() | 设置最佳看点 | 注意:不能设置回调 |
| app.camera.flyto() | 设置摄像机飞行 | |
| app.camera.flying | 判断摄影机是否在飞行 | 返回布尔值 |
| app.camera.stopFlying() | 停止摄影机飞行 | |
| app.camera.ratateAround() | 让摄影机环绕某坐标点或某物体旋转飞行 | |
| app.camera.floowObject(obj) | 摄影机跟随物体 | |
| app.camera.stopFloowingObject() | 停止摄影机跟随物体 |
6.5.2.1 官方案例解析 - 飞行控制
这部分案例的效果如下:
代码如下:
6.5.2.2 官方案例解析 - 控制交互
这部分案例的效果如下:
代码如下:
6.5.2.3 官方案例解析 - 控制地球相机
这部分案例的效果如下:
代码如下:
总结
以上是生活随笔为你收集整理的数字孪生可视化开发技术(ThingJS)学习笔记的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: EasyClick IOS 自动化测试报
- 下一篇: java jsonproperty_将多