前言
工業物聯網在中國的發展如火如荼,網路基礎設施建設,以及工業升級的迫切需要都為工業物聯網發展提供了很大的機遇。中國工業物聯網企業目前呈現兩種發展形式並存狀況:一方面是大型通訊、IT企業的佈局;一方面是傳統工業軟體和工業網路企業自發地延伸,由產品提供商發展為方案供應商。什麼叫做裙房?裙房是指附屬於主高樓並與之連成一體的低層建築。本文的 Demo 是針對於裙房做的,但是在工業監控系統中有很多雷同的部分,比如動畫、點選切換、點選隱藏、故障展示、開關、資料展示等等,都是比較通用的一些功能。所以針對這個 Demo 將這些內容做一個記錄,在這個 Demo 中我也遇到了一些問題,如何解決的都會拿出來跟大家分享。
http://www.hightopo.com/demo/annexMonitor/
程式碼實現
整個 Demo 我們要實現的部分有:
- 3D 場景的搭建
- 場景中葉輪的轉動
- 容器水位的上升下降
- 葉輪轉動故障展示
- 開啟/關閉動畫
- 燈光的開啟/關閉
- 點選切換模型
- 點選隱藏/顯示屬性視窗
- 文字內容/顏色變換
上面的功能看起來蠻多的,實際上實現起來還是比較容易的,總共就用了 200+ 行的程式碼。
3D 場景搭建
三維場景地基的搭建就 2 行程式碼:
var g3d = new ht.graph3d.Graph3dView();// Hightopo 的 3D 元件(三維場景地基) g3d.addToDOM();// 將 3D 組將新增到 body 體中
addToDOM = function(){ var self = this, view = self.getView(),//獲取元件的底層 div style = view.style; document.body.appendChild(view);//將元件底層div新增進body中 style.left = '0';//ht 預設將所有的元件的position都設定為absolute絕對定位 style.right = '0'; style.top = '0'; style.bottom = '0'; window.addEventListener('resize', function () { self.iv(); }, false);//視窗大小改變事件,呼叫重新整理函式 }
接下來我們要向場景中新增各種模型,用程式碼生成模型是非常無敵痛苦的,我們將整個場景的模型都放到一個 JSON 檔案中,並通過 ht.Default.xhrLoad 方法將這個 JSON 轉換為 3D 場景顯示在介面上:
var dm = g3d.dm();// 獲取 HT 3D 元件的資料容器 ht.Default.xhrLoad('scenes/system.json', function(text) { dm.deserialize(text);// 將函式的 text(json)引數傳給 deserialize 反序列化方法,可將 json 內容中的元素新增到 dataModel 資料容器中進行顯示 }
ht.Default.xhrLoad 方法是一個非同步載入 json 檔案的方法,第一個引數為傳入的 json 檔案,路徑是相對於 html 檔案的,第二個引數是回撥函式,在傳入的 json 檔案解析完畢之後做的操作。此方法為非同步載入,因此需要對 dm 資料容器中的資料進行獲取或操作的話,需要將獲取/操作的程式碼寫在 dm.deserialize(text) 方法之後,因為此時 dm 資料容器中才有節點。
上面將 JSON 檔案發序列化到 dm 資料容器中後介面顯示如下:
上圖中整個場景的背景是我後期用程式碼新增的,通過前面的 addToDOM 函式可以知道我們可以通過 getView 方法獲取 HT 3D 元件的底層 div,因此要在此 div 上新增一張背景圖也就不難了。剩下的 3D 模型部分都是由 JSON 反序列化出來的。
場景中葉輪的轉動
當然轉動不可能是整個模型在轉動,而是中間的“滾輪”在轉動,這要求設計師在建立模型的時候就將這個部分分離出來,然後我給此部分設定 tag 唯一標識為“yelun”,通過 dm.getDataByTag('yelun') 即可獲取到這個節點,然後給這個節點設定旋轉動畫。
HT 中排程進行的流程是,先通過 DataModel(https://hightopo.com/guide/guide/core/datamodel/ht-datamodel-guide.html) 新增排程任務,DataModel 會在排程任務指定的時間間隔到達時, 遍歷 DataModel 所有圖元回撥排程任務的 action 函式,可在該函式中對傳入的 Data 圖元做相應的屬性修改以達到動畫效果。
根據上面對排程任務的說明,我們瞭解到向 dm 資料容器中新增排程任務會遍歷整個資料容器,資料容器中內容不多的時候可能感覺不到,但當資料容器中內容多且模型重的情況下,對 dm 資料容器進行過濾就非常有必要了,而且如果新增多個排程任務都遍歷了整個資料容器,那麼對電腦的效能要求可想而知。我一開始使用的時候就是遺漏了對 dm 資料容器的過濾,因為場景不大,所以一開始沒有感覺,後來加了燈光後很重,就立馬出現問題了,但是一直找不到原因,後來在高人指點下才發下遺漏了對 data 的過濾判斷。
因此,排程任務傳入的引數物件中 action 方法傳入了一個 data 值,用於設定當前動畫的物件,不是此物件的直接可以 return 掉,不做任何操作:
var task = []; var yelun = dm.getDataByTag('yelun');// 獲取 tag 為 yelun 的節點 // 建立一個動畫排程任務 task.yelunTask = { interval: 100,// 動畫持續時間 action: function(data) {// 動畫內容 if (data !== yelun) return; // 設定 yelun 節點的 x 軸旋轉為當前 x 軸旋轉值再加上 Math.PI/12 yelun.setRotationX(yelun.getRotationX() + Math.PI/12); } } dm.addScheduleTask(task.yelunTask);// 將排程任務新增到資料容器中
容器水位的上升下降
這裡將容器水位的上升下降放到一個動畫排程任務裡了,也就是說通過 dm 資料容器操作這個排程任務就能夠同時操作這兩個部分的動畫,將上一小節中的 yelunTask 排程任務的 action 更改一下,因為上面的程式碼只對 yelun 節點進行了操作,我們需要對裝水的容器也進行操作。首先獲取裝水的容器,這裡將這個節點的唯一標識 tag 設定為“cylinder”:
var cylinder = dm.getDataByTag('cylinder');
然後更改排程中的 action 部分程式碼:
action: function(data) { if (!(data === yelun || data === cylinder)) return; // 葉輪轉動 yelun.setRotationX(yelun.getRotationX() + Math.PI/12); // 容器水位變化 if (cylinder.getTall() === 100) { cylinder.setTall(0);// 容器水位高度到達 100 的值時,重置為 0 } else cylinder.setTall(cylinder.getTall() + 1); }
葉輪轉動故障展示
因為沒有資料的傳輸,所以這邊故障資訊我只能自己造假資料了,我建立了一個 10 以內的整數隨機數,判斷這個值是否為 1,如果為 1 就將運作正常的圖示變換成告警圖示,同時我還通過這個值來設定 dm 資料容器新增/移除排程任務來控制當前葉輪轉動/停止、容器水位變化與否:
var alarm = dm.getDataByTag('alarm');// 獲取告警圖示節點 setInterval(function() { var random = Math.floor(Math.random()*5); if (random === 1) { alarm.s('shape3d.image', 'symbols/電信/故障 2.json');// 設定告警圖示節點為“故障”圖示 dm.removeScheduleTask(task.yelunTask);// 將葉輪的動畫加上 } else { alarm.s('shape3d.image', 'symbols/電信/正常 2.json');// 設定告警圖示節點為“正常”圖示 dm.addScheduleTask(task.yelunTask);// 移除葉輪的動畫 } }, 1000);
開啟/關閉動畫
上一小節我們已經提到了開啟/關閉動畫的方式,這邊我們運用 form 表單,手動操作動畫的開啟和關閉(注:這裡只說明第一行的“水流開關”)。
首先,我們需要建立一個 formPane 表單元件(https://hightopo.com/guide/guide/plugin/form/ht-form-guide.html),在這個表單元件中新增行資料,這邊操作動畫的開啟和關閉我是用的 checkbox,值變化只有 true 和 false,這個情況用這個是比較優的選擇。然後通過監聽這個 checkbox 的值的變化事件,設定動畫的開啟(新增)或者關閉(移除)。
function createForm(task) { var form = new ht.widget.FormPane();// 建立 form 表單元件物件 form.setWidth(160);// 設定表單元件的寬度 form.setHeight(90);// 設定表單元件的高度 // 設定表單元件底層 div 的樣式屬性 form.getView().style.right = '10px'; form.getView().style.top = '10px'; form.getView().style.background = 'rgba(255, 255, 255, 0.2)'; form.getView().style.borderRadius = '5px'; document.body.appendChild(form.getView());// 將 form 表單底層 div 新增到 body 體中 // 水閥開啟和關閉 form.addRow([// 給 form 表單新增一行資料 { checkBox: {// 核取方塊類,HT 將此封裝到 form 中 實際上建立了一個 ht.widget.CheckBox 元件 label: '水流開關',// 設定 checkbox 的文字內容 labelColor: '#fff',// 設定 checkbox 文字顏色 selected: true,// 設定此 checkbox 是否選中 onValueChanged: function() {// 監聽值變化事件 if (this.isSelected()) dm.addScheduleTask(task.arrowTask);// 如果這個 checkBox 選中,則新增動畫(開啟水閥) else dm.removeScheduleTask(task.arrowTask);// 如果這個 checkBox 未被選中,則移除動畫(關閉水閥) } } } ], [0.1]);// 設定這個行資料中列的寬度 return form; }
addRow 方法上面程式碼中一言兩語解釋不清楚,參考如下說明:
addRow(items, widths, height, params) 新增一行元件
- items為元素陣列,元素可為字串、json 格式描述的元件引數資訊、html 元素或者為 null 的空
- widths 為每個元素寬度資訊陣列,寬度值大於 1 代表固定絕對值,小於等於 1 代表相對值,也可為 80+0.3 的組合
- height 為行高資訊,值大於 1 代表固定絕對值,小於等於 1 代表相對值,也可為 80+0.3 的組合,為空時採用預設行高
- params 為 json 格式的額外引數,例如插入行索引以及行邊框或背景顏色等,如{index: 2, background: 'yellow', borderColor: 'red'}
上面程式碼中提到的 arrowTask 是對場景中的“箭頭”流動新增的動畫排程任務,通過控制 form 表單中 checkbox 核取方塊是否選中可直接操作 dm 是否新增/移除動畫排程任務。
燈光的開啟/關閉
控制燈光的開啟和關閉,這裡也是通過 form 表單上的 checkbox 核取方塊來進行操作的。一般建議不要使用燈光,渲染太燒效能了,這裡只是為了效果而新增做一個說明。
首先我們需要建立一個“燈”節點,然後通過設定樣式屬性 setStyle 來設定燈的型別、顏色、燈照範圍等等屬性:
// 新增燈光 var light = new ht.Light();// 建立一個燈節點(繼承於 ht.Node) (https://hightopo.com/guide/guide/core/lighting/ht-lighting-guide.html) light.p3([15, 120, 50]);// 設定此節點的位置 light.setTag('light');// 設定此節點的唯一標識 dm.add(light);// 將此節點新增到 dm 資料容器中進行顯示 light.s({// 設定此節點的樣式屬性 setStyle 簡寫為 s 'light.type': 'point',// 設定燈型別 'light.color': 'rgb(252,252,149)',// 設定燈顏色 'light.range': 1400,// 設定燈照範圍 '3d.visible': false// 設定此節點在 3d 上不可見 });
然後在 form 表單上新增一行用來控制燈的開關、燈的顏色燈功能:
// 9、燈光開啟和關閉 以及顏色切換 form.addRow([// form 中新增一行 { id: 'lightDisabled',// 設定此項的 id 值,可通過 form.getItemById 獲取此項 checkBox: {// 核取方塊元件 label: '開關燈',// 設定核取方塊文字內容 labelColor: '#fff',// 設定核取方塊文字顏色 selected: true,// 設定核取方塊是否選中 onValueChanged: function() {// 監聽值變化事件 dm.getDataByTag('light').s('light.disabled', !this.getValue());// 獲取燈節點並設定是否關閉燈光效果,light.disabled 屬性預設為false,可設定為true關閉燈效果 } } }, { colorPicker: {// 顏色選擇器元件 value: 'rgb(252,252,149)',// 設定當前值 instant: true,// 設定是否處於即時狀態,將會實時改變模型值 onValueChanged: function() {// 監聽值變化事件 dm.getDataByTag('light').s('light.color', this.getValue())// 設定燈的顏色為當前選中的顏色 } } } ], [0.1, 0.1]);
點選切換模型
HT 將事件監聽封裝到 mi 事件(https://hightopo.com/guide/guide/core/3d/ht-3d-guide.html#ref_interactionlistener)中,mi 方法中有多種事件,這裡我們需要的是單擊節點的事件監聽 clickData 事件,通過判斷事件型別 e.kind 是否為 clickData,之後對節點的設定模型即可:
var waterPump6 = dm.getDataByTag('水泵06');// 獲取 tag 為“水泵06”的節點 waterPump6.s({// 設定該節點的樣式屬性 'note': '點我切換模型',// 設定標註文字內容 'note.transparent': true,// 設定標註在 3D 下是否透明 'note.t3': [0, 0, -50],// 設定標註在 3D 下的偏移 'note.reverse.flip': true//設定標註背面是否顯示正面的內容 }); g3d.mi(function(e) {// 監聽 3D 元件上的事件 if(e.kind === 'clickData') {// 點選節點事件 // 模型點選切換 if (e.data === waterPump6 && e.data.s('shape3d') === 'models/裙房系統/水泵.json') e.data.s('shape3d', 'models/fengji.json');// 設定點選節點的 shape3d 樣式屬性 else if (e.data === waterPump6 && e.data.s('shape3d') === 'models/fengji.json') e.data.s('shape3d', 'models/裙房系統/水泵.json');// 設定點選節點的 shape3d 樣式屬性 } });
HT 設定模型是通過設定節點的樣式屬性 node.setStyle(簡寫為 node.s)為 shape3d 來實現的。
點選隱藏/顯示屬性視窗
上面說到了事件的監聽,既然同為點選事件,我們就在一個監聽事件裡面進行具體的操作即可,在上面的 if (e.kind === 'clickData') 判斷中新增顯示/隱藏屬性視窗的邏輯:
var waterPump5 = dm.getDataByTag('水泵05'); waterPump6.s({ 'note': '點我切換模型', 'note.transparent': true, 'note.t3': [0, 0, -50], 'note.reverse.flip': true }); g3d.mi(function(e) { if(e.kind === 'clickData') { // 模型點選切換 if (e.data === waterPump6 && e.data.s('shape3d') === 'models/裙房系統/水泵.json') e.data.s('shape3d', 'models/fengji.json'); else if (e.data === waterPump6 && e.data.s('shape3d') === 'models/fengji.json') e.data.s('shape3d', 'models/裙房系統/水泵.json'); // 模型點選 隱藏/顯示屬性視窗 if (e.data === waterPump5) {// 判斷點選的圖元是否為 waterPump5 if(giveWater.s('3d.visible')) {// 判斷當前屬性視窗是否為顯示狀態 giveWater.s('3d.visible', false);// 設定屬性視窗不可見 e.data.s('note', '點我顯示屬性視窗');// 更改標註中的顯示內容 } else { giveWater.s('3d.visible', true);// 設定屬性視窗可見 e.data.s('note', '點我隱藏屬性視窗')// 更改標註中的顯示內容 } } } });
文字內容/顏色變換
通過 tag 獲取場景中對應的屬性視窗的節點,此節點為一個皮膚,相當於六面體有六面,這個節點型別就只有一面,並通過設定屬性 shape3d.image 設定此節點上的圖片為 tooltips.json 向量圖示(https://hightopo.com/guide/guide/core/vector/ht-vector-guide.html)。向量在 Hightopo(HT)中是向量圖形的簡稱,常見的 png 和 jpg 這類的柵格點陣圖, 通過儲存每個畫素的顏色資訊來描述圖形,這種方式的圖片在拉伸放大或縮小時會出現圖形模糊,線條變粗出現鋸齒等問題。 而向量圖片通過點、線和多邊形來描述圖形,因此在無限放大和縮小圖片的情況下依然能保持一致的精確度。而且 HT 的向量圖形還有一個非常重要的特點,就是能夠對向量圖形上的任何一個部分都進行資料繫結,也就是說上圖中的五張圖,我們可以只繪製一張圖,通過資料繫結來改變這張圖上的文字以及數值內容。
向量圖示中的資料繫結可以用在工業中的生產看板、大屏中的資料顯示等等,都能夠以一種高效的方式進行產品的整合。
向量圖形的資料繫結能夠再寫一篇文章進行闡述了,這裡就不多提,大家自行去官網上檢視“向量手冊”以及“資料繫結手冊”,說明的比較詳細。
獲取到對應的節點之後,通過 node.a 方法可以獲取和設定資料繫結(https://hightopo.com/guide/guide/core/databinding/ht-databinding-guide.html#ref_vector)的屬性,這裡我們繫結的是文字內容“label”和數值“value”以及數值顏色“valueColor”:
var billboardArray = []; // 通過 tag 獲取節點 var temperature1 = dm.getDataByTag('回水溫度1');// 獲取 tag 為"回水溫度1"的節點 billboardArray.push(temperature1); var temperature2 = dm.getDataByTag('回水溫度2'); billboardArray.push(temperature2); var returnPress = dm.getDataByTag('回水壓力'); billboardArray.push(returnPress); var givePress = dm.getDataByTag('供水壓力'); billboardArray.push(givePress); var giveTemp = dm.getDataByTag('供水溫度'); billboardArray.push(giveTemp); var giveWater = dm.getDataByTag('供水流量'); billboardArray.push(giveWater); // 文字標籤內容變換 billboardArray.forEach(function(billboard) { billboard.a('label', billboard.getTag());// 設定資料繫結屬性為 label 的屬性值為當前節點的 tag 內容 }); // 文字標籤數字變換+顏色變換 更改圖示中繫結的 value 屬性值 setInterval(function() { billboardArray.forEach(function(billboard) { var random = Math.random()*100; billboard.a('value', random.toFixed(2)); // 設定圖示中“數值內容顏色” if (random > 70 && random <= 80) billboard.a('valueColor', '#00FFFF'); else if (random > 80 && random <= 90) billboard.a('valueColor', '#FFA000'); else if (random > 90) billboard.a('valueColor', '#FF0000'); else billboard.a('valueColor', ''); }); }, 1000);