第5章〓ThingJS进阶 本章将重点介绍ThingJS数字孪生应用开发的进阶部分,包括组件、插件、预制件的开发方法,以及场景层级控制、数据对接和界面展示。 7min 5.1组件 ◆ 5.1.1组件的定义 图51对象和组件的关系 组件(Component)是一种对象功能的扩展方式。组件是对象的组成部分,提供物体的生命周期方法,对象和组件的关系如图51所示。 5.1.2组件的作用和生命周期 使用组件开发,可以大大地减少代码中的重复部分,提高代码的质量和效率。简单地讲,组件的生命周期是指从组件创建到组件销毁的过程,组件生命周期如图52所示。 图52组件生命周期 5.1.3组件开发 下面通过一个具体案例来介绍如何编写代码,以便实现一个简单的自定义组件。 【例51】创建一个可以让对象旋转的自定义组件,先将组件添加到立方体上,再通过添加按钮来实现禁用、启用、卸载该组件的功能,代码如下: //教材源代码/examples/component/ComponentRotator.html //创建自定义组件MyRotator class MyRotator extends THING.Component { onAwake(params) { //获取旋转的速度 this.speed = params.speed } onUpdate(deltaTime) { //让对象旋转 this.object.rotateY(this.speed * deltaTime) } } //初始化程序 const app = new THING.App(); //创建立方体 const box = new THING.Box(3, 3, 3); //给立方体添加组件 box.addComponent(MyRotator,'rotator',{speed: 10 }) //添加禁用组件按钮 new THING.widget.Button('禁用组件',function () { box.rotator.enable = false }); //添加启用组件按钮 new THING.widget.Button('启用组件',function () { if( !box.rotator){ console.log('The Rotator Component has been removed.') return } box.rotator.enable = true }); //添加卸载组件按钮 new THING.widget.Button('卸载组件',function () { box.removeComponent('rotator') }); 注意: deltaTime是当前帧距上一帧之间的时间,可用于设置动画播放在时间上的准确性。 编写完代码后,保存为ComponentRotator.html文件。启动HTTP服务,预览运行效果,如图53所示。可以看到立方体可以逆时针旋转,当单击禁用组件按钮时,立方体停止旋转; 当单击启用组件按钮时,立方体恢复旋转; 当单击卸载组件按钮时,立方体停止旋转。卸载组件后,单击启用组件无效。 图53自定义组件的运行效果 在上面的例子中,当使用addComponent给立方体添加了自定义组件时,需要传入参数,这里传入的参数分别为自定义组件类名MyRotator、自定义组件的名字rotator、自定义组件的参数(旋转速度speed)等,代码如下: obj.addComponent(MyRotator,'rotator',{speed: 10 }); 如果需要给查询到的所有对象添加组件,则可以通过下面的方法实现,代码如下: var objs = app.query(*); objs.addComponent(MyRotator,{speed: 10 }); 另外,还可以导出自定义的组件的常用属性或方法,并将其直接作用在对象上,代码如下: class MyRotator extends THING.Component { //需要导出的属性 static exportProperties = [ 'speed' ] //需要导出的方法 static exportFunctions = [ 'setSpeed' ] speed = 10; setSpeed(value) { this.speed = value; } } const box = new THING.Box(); box.addComponent(MyRotator); box.speed = 50; //直接访问成员 box.setSpeed(100); //直接调用方法 通过这个例子,让大家对组件的开发和使用有了一定的认识。 8min 5.2预制件 ◆ 5.2.1预制件介绍 预制件是一个预先制定好的资源,是具有一定属性、行为、效果的对象模板。预制件被用于创建大量重复的对象,例如在一个场景中有多个叉车对象,每个叉车对象都有移动、搬运、装载等行为,那么就可以制作一个叉车预制件以供重复使用。如果只创建一个对象,也可以使用预制件。 5.2.2预制件开发 在4.1.3节中,介绍了用Blender导出场景文件(.gltf格式的文件),在预制件开发中,同样需要用到场景文件。首先打开Blender,在场景中添加一个立方体,然后给立方体添加自定义属性。 先添加一个type属性,属性类型选择字符串型,属性值为Box。再添加一个components属性,属性类型选择Python,属性值为[{"type": "PrefabRotator","name": "rotator"}],此时立方体就具有了Box类型和PrefabRotator预制件属性,如图54所示。 图54给立方体添加属性 接着选择场景,新建一个自定义属性type,属性类型选择字符串型,属性值为prefab,添加预制件属性的过程如图55所示。 图55添加预制件属性的过程 按前面介绍的方法导出.gltf文件,导出时记得要勾选自定义属性,将导出的文件命名为prefab.gltf,然后打开prefab.gltf文件,加入extensionsUsed和extensions两个配置项,代码如下: "extensionsUsed": [ "TJS_component" ], "extensions": { "TJS_component": { "files": [ "./PrefabRotator.js" ] } }, extensionsUsed是指额外的拓展引用,类型为数组,这里只需填写TJS_component,此名称不可更改。extensions是指具体的拓展文件配置,类型为对象。这里填写和上面一致的TJS_component即可,然后添加files,类型为数组,将所需拓展的文件路径填写在此。那么这里使用的PrefabRotator.js文件应该如何编写呢?下面通过一个案例来介绍如何编写PrefabRotator.js。 【例52】编写一个让对象旋转的预制件.js文件,代码如下: //教材源代码/examples/prefab/PrefabRotator.js class PrefabRotator extends THING.Component { constructor() { super() //定义props属性 this.props = { speed: { type: 'number', value: 10 } } } //添加一个setter来设置这个值 set speed(value) { this.props.speed.value = value } //添加一个getter来获取这个值 get speed(){ return this.props.speed.value; } onUpdate(deltaTime) { this.object.rotateY(this.props.speed.value * deltaTime) } //除了可以用setter设置组件中的值外,还可以使用一个函数来修改值,从而修改对象的行为 rotateSpeed(speed) { this.props.speed.value = speed } } //将组件注册到ThingJS THING.Utils.registerClass( 'PrefabRotator', PrefabRotator); 编写完代码后,保存为PrefabRotator.js文件,预制件就完成了。 【例53】预制件完成后应如何加载预制件呢?下面通过编写HTML文件来加载预制件,新建一个PrefabRotator.html文件,在 JQuery的AJAX请求对JSONP也进行了封装,因此可以直接使用相关方法请求 JSONP数据,请求格式的代码如下: $.ajax({ type: "get", url: "https://3dmmd.cn/monitoringData", data: {"id": 1605 }, dataType: "jsonp", jsonpCallback: "callback", success: function (d) { console.log(d.data) } }); dataType: 返回的数据类型,注意这里必须设置为JSONP方式; jsonpCallback: 返回数据中的回调函数名,其他参数同AJAX数据对接方式。 3. WebSocket 由于并非所有浏览器都支持WebSocket,所以在使用WebSocket之前,需要通过window.WebSocket来判断当前浏览器的支持情况。当window.WebSocket == true时,可使用如下接口,进行数据对接。 1) WebSocket(url,[protocol] ) WebSocket的构造函数,用于创建一个WebSocket实例。第1个参数url用于指定连接的 URL。第2个参数 protocol 是可选的,用于指定可接受的子协议。 2) send(msg) WebSocket发送消息的接口,该接口用于主动关闭连接。该接口只有一个参数,该参数为一个String、ArrayBuffer或者Blob类型的数据,用于表示发送的信息,该信息会被发送到服务器端。 3) close(code) WebSocket的关闭连接接口,参数为一个可选的关闭状态号,常见的关闭状态号如下。 (1) 1000: 表示正常关闭(默认值)。 (2) 1001: 表示离开,例如服务器出现故障,浏览器离开了打开连接的页面。 (3) 1002: 表示协议错误。 (4) 1003: 表示由于接收到不允许的数据类型而断开连接。 4) onXXX事件 WebSocket的监听事件接口,可以设置为一个回调函数来处理相关业务逻辑; 具体支持为onopen、onmessage、onerror、onclose等,分别对应连接打开、收到消息、建立与连接过程中发生错误和连接关闭事件。 4. MQTT 目前支持MQTT协议的JS有很多,比较推荐的是mqtt.js,其功能完善,并且支持的平台较多。mqtt.js的相关API如下。 (1) mqtt.connect([url],options): MQTT连接接口,连接到指定的 MQTT Broker,并返回一个 Client 对象。第1个参数用于传入一个 URL 值,该URL指向Broker代理。第2个参数为一个连接的可选配置信息,具体支持的参数有keepalive、clientId、connectTimeout等。 (2) Client.publish(topic,message,[options],[callback]): Client对象发布消息接口,用于Client对象向某个topic发布消息。第1个参数为发送的topic; 第2个参数为发送的消息; 第3个参数为发布消息的可选配置信息,具体支持Qos、Remain等; 第4个参数为发布消息后的回调函数,当发布成功时函数无参数,当发布失败时回调函数有error参数。 (3) Client.subscribe(topic/topic array/topic object,[options],[callback]): Client对象订阅消息接口,用于Client对象向某个或者某些topic订阅消息。第1个参数为订阅的topic或topic数组; 第2个参数为订阅消息的可选配置信息; 第3个参数为订阅消息后的回调函数,当订阅成功时函数无参数,当订阅失败时回调函数有error参数。 (4) Client.unsubscribe(topic/topic array,[options],[callback]): Client对象取消订阅消息接口,用于Client对象取消某个或者某些topic的订阅消息。第1个参数为取消订阅的topic或topic数组; 第2个参数为取消订阅消息的可选配置信息; 第3个参数为取消订阅消息后的回调函数,当取消成功时函数无参数,当取消失败时回调函数有error参数。 (5) Client.end([force],[options],[callback]): Client对象关闭接口,用于关闭当前客户端。第1个参数为是否立即关闭客户端,true表示立即关闭,false表示需要等待断开链接的消息被接收后关闭客户端; 第2个参数为关闭客户端时的可选配置信息,具体支持ReasonCode等;第3个参数为关闭客户端时的回调函数。 (6) Client.on(key,callback): Client对象的监听事件接口,用于监听一个或多个常用的事件。第1个参数为字符串类型的事件类型,具体支持connect、disconnect、reconnect、message、error、end等。 5.5.3数据对接案例 【例56】使用WebSocket进行数据对接,实现当按下开启按钮时,每隔5s读取信息,并在控制台打印消息; 当按下“关闭”按钮时,返回“WebSocket关闭”,实现代码如下: //教材源代码/examples/DataWebSocket.html let webSocket = null; let startReading = function () { if (!webSocket) { webSocket = new WebSocket('wss://3dmmd.cn/wss'); webSocket.onopen = function () { console.log("WebSocket服务器连接成功"); }; webSocket.onmessage = function(evt) { var data = evt.data; console.log(data); }; webSocket.onclose = function (evt) { console.log("WebSocket关闭"); webSocket = null; }; } } //关闭连接 let stopReading = function () { if(webSocket) { webSocket.close(); webSocket = null; } } //初始化程序 const app = new THING.App(); //创建立方体 const box = new THING.Box(5, 5, 5); new THING.widget.Button('开启读取', function () { startReading(); }); new THING.widget.Button('关闭读取', function () { stopReading(); }); 在给出的WebSocket对接案例中,提供了两个按钮,分别是“开启读取”按钮和“关闭读取”按钮。 当单击“开启读取”按钮时会触发执行updateData函数,该函数会判断webSocket是否存在,若不存在,则客户端通过构造WebSocket对象打开一个URL为wss://3dmmd.cn/wss的链接,同时定义webSocket对象的onopen、onmessage和onclose事件响应函数。 当连接建立时,webSocket对象会触发Open事件,调用onopen函数,在浏览器的控制台打印“WebSocket服务器连接成功”连接消息。该连接设定每隔5s向客户端推送一次数据,因此连接建立之后,webSocket对象会每隔5s触发一次Message事件,同时每隔5s调用一次onmessage函数,在浏览器的控制台每隔5s打印一次data内容。 当单击“关闭读取”按钮时会触发执行stopUpdate函数,webSocket对象调用close函数关闭链接并置空。当链接关闭时,webSocket对象触发Close事件,调用onclose函数,在浏览器的控制台打印“WebSocket关闭”消息。 启动HTTP服务后预览运行效果,按F12键调出开发者工具,选择控制台,当按下“开启读取”按钮时,控制台打印消息,每隔5s读取信息; 当按下“关闭读取”按钮时,返回“WebSocket关闭”,如图510所示。 图510WebSocket数据对接 7min 5.6界面展示 ◆ 5.6.1Marker 第4章介绍了对象标记,给立方体添加了Marker对象。添加Marker也是一种常用的界面展示方式。将Marker作为子对象添加到指定对象上,设置Marker的相对位置,使其随对象一同移动。Marker默认为受距离远近影响,呈现近大远小的三维效果,也会在三维空间中实现前后遮挡。这里,介绍一种绘制canvas并添加Marker的方法。 【例57】绘制canvas,将绘制完成的canvas转换为图片对象并添加到Marker上,代码如下: //教材源代码/examples/MarkerCanvas.html //初始化程序 let app = new THING.App(); //创建立方体 let box = new THING.Box(); //将canvas转换为图片对象 let image = new THING.ImageTexture( {resource: createTextCanvas('88') }) let marker = new THING.Marker({ name: "marker", parent: box, localPosition: [0, 3, 0], scale: [2, 2, 2], style: { image: image } }) //绘制canvas function createTextCanvas(text) { const canvas = document.createElement("canvas"); canvas.width = 64; canvas.height = 64; const ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "rgb(32, 32, 256)"; ctx.beginPath(); ctx.arc(32, 32, 30, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = "rgb(255, 255, 255)"; ctx.lineWidth = 4; ctx.beginPath(); ctx.arc(32, 32, 30, 0, Math.PI * 2); ctx.stroke(); ctx.fillStyle = "rgb(255, 255, 255)"; ctx.font = "32px sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(text, 33, 36); return canvas; } 绘制canvas并转换为图片Marker对象的运行效果如图511所示。 图511绘制canvas并转换为图片Marker对象 5.6.2WebView 【例58】创建WebView对象,将ThingJS网页加载到三维场景中,设置页面属性,设置摄像头位置和目标位置,代码如下: //教材源代码/examples/WebView.html //初始化程序 let app = new THING.App(); //创建页面 let webView = new THING.WebView({ type: 'WebView', url: 'https://www.thingjs.com', position: [0, 0.5, 5], domScale:0.01, //网页缩放系数 domWidth: 1920, //页面高度,单位为px domHeight: 1080 //页面高度,单位为px }); //设置页面不可拾取交互 webView.pickable = false; //设置摄像头 app.camera.position=[0,0,20]; app.camera.target=[0,0,0]; 运行效果如图512所示。 图512加载页面 5.6.3ECharts ECharts是一个使用 JavaScript 实现的开源可视化库,提供了常规的折线图、柱状图、散点图、饼图等多种图表,并且支持图表与图表之间进行混合使用。提供交互丰富,可高度个性化定制的数据可视化图表。通过ECharts图表可以更直观地查看场景中的数据情况。在HTML页面中通过 【例59】使用ECharts创建全年平均温度、降水量、蒸发量变化图表,代码如下: //教材源代码/examples/Echarts.html const app = new THING.App(); //创建需要的 DOM 节点,DOM 节点为需要在场景中显示的节点 //背景颜色 let bottomBackground = document.createElement('div'); //标题 let bottomFont = document.createElement('div'); //图表 let bottomDom = document.createElement('div'); //背景样式左上角对齐 let backgroundStyle = 'top:0px; position: absolute;left:0px;height:400px;width:600px;background: rgba(41,57,75,0.74);'; //字体样式 let fontStyle = 'position: absolute;top:0px;right:0px;color:rgba( 113,252,244,1);height:78px;width:600px;line-height: 45px;text-align: center;top: 20px;'; //图表DIV样式 let chartsStyle = 'position: absolute;top:80px;right:0px;width:600px;height:300px;'; //设置样式 bottomBackground.setAttribute('style', backgroundStyle); bottomFont.setAttribute('style', fontStyle); bottomDom.setAttribute('style', chartsStyle); //标题文字 bottomFont.innerHTML = '温度降水量平均变化图'; //通过调用 window.echarts 获取 ECharts对象。通过 init 方法创建图表实例,传入的参数为需要 //ECharts 图表的 DOM 节点,返回的是图表实例 let bottomCharts = window.echarts.init(bottomDom) //配置图表的属性,图表的各项属性 options 代表的含义可以在 ECharts 官网中查询 let echartOptions = { "tooltip": { "trigger": "axis", "axisPointer": { "type": "cross", "crossStyle": { "color": "#999" } } }, "legend": { "textStyle": { "color": "auto" }, "data": [ "蒸发量", "降水量", "平均温度" ] }, "xAxis": [ { "axisLabel": { "textStyle": { "color": "#fff" } }, "type": "category", "data": [ "1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月" ], "axisPointer": { "type": "shadow" } } ], "yAxis": [ { "type": "value", "name": "水量", "min": 0, "max": 250, "interval": 50, "splitLine": { "lineStyle": { "type": "dotted" }, "show": true }, "nameTextStyle": { "color": "#fff" }, "axisLabel": { "textStyle": { "color": "#fff" }, "formatter": "{value} ml" } }, { "splitLine": { "lineStyle": { "type": "dotted" }, "show": true }, "type": "value", "name": "温度", "min": 0, "max": 25, "interval": 5, "nameTextStyle": { "color": "#fff" }, "axisLabel": { "textStyle": { "color": "#fff" }, "formatter": "{value} °C" } } ], "series": [ { "name": "蒸发量", "type": "bar", "data": [ 2, 4.9, 7, 23.2, 25.6, 76.7, 135.6, 162.2, 32.6, 20, 6.4, 3.3 ] }, { "name": "降水量", "type": "bar", "data": [ 2.6, 5.9, 9, 26.4, 28.7, 70.7, 175.6, 182.2, 48.7, 18.8, 6, 2.3 ] }, { "name": "平均温度", "type": "line", "yAxisIndex": 1, "data": [ 2, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3, 23.4, 23, 16.5, 12, 6.2 ] } ], "color": [ "#2b908f", "#90ee7e", "#f45b5b", "#7798BF", "#aaeeee", "#ff0066", "#eeaaee", "#55BF3B", "#DF5353", "#7798BF", "#aaeeee" ] } //调用setOptions 方法将配置好的 options 传入图表 bottomCharts.setOption(echartOptions); //将节点放到页面根节点下 bottomBackground.appendChild(bottomFont); bottomBackground.appendChild(bottomDom); document.querySelector('#div3d').appendChild( bottomBackground); 创建ECharts图表,运行效果如图513所示。 图513创建ECharts图表 12min 5.6.4Widget Widget是一个支持动态数据绑定的轻量级界面库。可以通过THING.widget界面库创建Button按钮、Banner通栏及Panel面板,其中Panel面板中可以添加滑动条、双向按钮、单选框、复选框、文字框等其他组件,通过修改组件值来达到动态修改场景中的对象属性的效果。HTML页面中通过 【例510】使用Widget库创建按钮,代码如下: //教材源代码/examples/WidgetButton.html let app = new THING.App(); //创建按钮 new THING.widget.Button('WidgetButton',()=>{ //单击按钮执行回调方法 console.log('WidgetButton') }) 图514创建按钮 运行后会在浏览器窗口的左上角添加按钮,如图514所示。 【例511】使用Widget库创建面板,代码如下: //教材源代码/examples/WidgetPanel.html const app = new THING.App(); //创建Panel面板 let panel = new THING.widget.Panel( { //设置面板样式 template: 'default', //角标样式 cornerType: "none", //设置面板宽度 width: "300px", //是否有标题 hasTitle: true, //设置标题名称 titleText: "我是标题", //面板是否允许有关闭按钮 closeIcon: true, //面板是否支持拖曳功能 dragable: true, //面板是否支持收起功能 retractable: true, //设置透明度 opacity: 0.9, //设置层级 zIndex: 99 }); //定义面板数据 let dataObj = { pressure: "0.14MPa", temperature: "21°C", checkbox: { 设备1: false, 设备2: false, 设备3: true, 设备4: true }, radio: "摄像机01", open1: true, height: 10, maxSize: 1.0, iframe: "https://www.thingjs.com", progress: 1, img: "https://www.thingjs.com/guide/image/new/logo2x.png", button1: false, button2:true }; //向Panel面板中添加组件 let press = panel.addString(dataObj, 'pressure').caption('水压').isChangeValue(true); let height = panel.addNumberSlider(dataObj, 'height').caption('高度').step(10).min(0).max(100).isChangeValue(true).on('change',function(value){ dataObj.height = value; }); let open1 = panel.addBoolean(dataObj, 'open1').caption('开关01'); let radio = panel.addRadio(dataObj, 'radio', ['摄像机01', '摄像机02']); let check = panel.addCheckbox(dataObj, 'checkbox').caption( { "设备2": "设备2( rename)" }); let iframe = panel.addIframe(dataObj, 'iframe').caption('视屏'); let img = panel.addIframe(dataObj, 'img').caption('图片'); let button1 = panel.addImageBoolean(dataObj, 'button1').caption('仓库编号').url('https://www.thingjs.com/static/images/sliohouse/warehouse_code.png'); //可以通过font标签设置caption颜色 let button2 = panel.addImageBoolean(dataObj, 'button2').caption( '温度检测').url('https://www.thingjs.com/static/images/sliohouse/temperature.png'); 创建面板,运行效果如图515所示。 图515创建面板 【例512】使用Widget库创建Tab面板,代码如下: //教材源代码/examples/WidgetTable.html const app = new THING.App(); let panel = THING.widget.Panel( { template: "default", hasTitle: true, titleText: "粮仓信息", closeIcon: true, dragable: true, retractable: true, width: "380px" }); //定义面板数据 let dataObj = { '基本信息': { '品种': "小麦", '库存数量': "6100", '保管员': "张三", '入库时间': "19:02", '用电量': "100", '单仓核算': "无" }, '粮情信息': { '仓房温度': "26", '粮食温度': "22" }, '报警信息': { '温度': "22", '火灾': "无", '虫害': "无" }, }; panel.addTab(dataObj); 创建Tab面板,运行效果如图516所示。 图516创建Tab面板 【例513】通过Widget库中的Banner 组件创建一个通栏,代码如下: //教材源代码/examples/WidgetBanner.html const app = new THING.App(); let banner_left = new THING.widget.Banner( { //通栏类型: top 为上通栏(默认), left 为左通栏 column: 'left' }); //引入图片文件 let baseURL = "https://www.thingjs.com/static/images/sliohouse/"; //数据对象,用于为通栏中的按钮绑定数据 let dataObj = { orientation: false, cerealsReserve: false, video: true, cloud: true }; //向左侧通栏中添加按钮 let img5 = banner_left.addImageBoolean(dataObj, 'orientation').caption('人车定位').imgUrl(baseURL + 'orientation.png'); let img6 = banner_left.addImageBoolean(dataObj, 'cerealsReserve').caption('粮食储存').imgUrl( baseURL + 'cereals_reserves.png'); let img7 = banner_left.addImageBoolean(dataObj,'video').caption('视频监控').imgUrl(baseURL + 'video.png'); let img8 = banner_left.addImageBoolean(dataObj,'cloud').caption('温度云图').imgUrl(baseURL + 'cloud.png'); //为按钮绑定事件 img5.on('change', function (value) { //当按钮值改变时触发 console.log(value) }) //根据页面调整布局 $('.ThingJS_wrap').css('position','absolute').css('top','0px') 创建通栏,运行效果如图517所示。 图517创建通栏 6min 5.6.5CSS组件 CSS组件提供了将HTML/CSS元素添加到三维场景的能力,包括CSS2DComponent和CSS3DComponent。 在渲染方式上,CSS3DComponent支持设置界面的渲染类型包括精灵渲染方式和平面渲染方式,CSS2DComponent只支持精灵渲染方式。在性能方面上,CSS2DComponent的性能更高。 【例514】使用CSS2DComponent组件给立方体添加一个HTML界面,代码如下: //教材源代码/examples/CSS2D.html //创建HTML面板 const sign = `