基於 HTML5 的 3D 工業互聯網展示方案

基於 HTML5 的 3D 工業互聯網展示方案

來自專欄極樂科技31 人贊了文章

前言

通用電氣(GE)、IBM、英特爾等公司主推的「工業互聯網」正在經歷「產品-數據分析平台-應用-生態」的演進。這主要得益於 Predix 數據分析平台對工業互聯網應用的整合能力。Predix 就像工業數據領域的 iOS 或者安卓系統一樣,能夠讓工程師自己建立模型和應用,打通前方數以萬計的感測器和後方每天增加超過 5000 萬條的資料庫。

在實際應用中,東方航空公司在 Predix 上使用工業互聯網應用搜集了 500 多台 CFM56 發動機的高壓渦輪葉片保修數據,結合遠程診斷紀錄和第三方數據,建立了葉片損傷分析預測模型。從前,航空公司需要定期強制飛機「休病假」,把微型攝像頭伸入發動機內進行檢查。現在,只要根據數據分析平台上的結果就可以預測發動機的運行情況,定製科學的重複檢查間隔,提升運營效率。除去航空領域,工廠倉庫的監管也是非常需要互聯網的介入,不僅能夠實時監控倉庫當前的數據和信息,還能夠降低倉庫監管人員的數量,更能夠預測倉庫故障信息並提前告知工作人員採取對應的措施,能夠有效地避免工廠運營暫停導致的損失。

hightopo.com/demo/wareh

代碼生成

這個例子是採用 es6 的模塊化的方式部署的。打開 index.html 進入 lib/index.js,源碼是在 src 文件夾中,我們直接進 src/view 下的 index.js

在頂部載入其他模塊中含有 export 介面的模塊:

import sidebar from ./sidebar.js;import header from ./header.js;import BorderLayout from ./common/BorderLayout.js;import shelfPane from ./common/shelfPane.js;import chartPane from ./common/chartPane.js;import graph3dView from ./3d/index;

場景布局

我們將頁面上的每個部分分開來放在不同的 js 文件中,就是上面載入的 js export 的部分,根層容器 BorderLayout(整體最外層的 div),整張圖上的部分都是基於 borderLayout 的。

  • 最外層容器 BorderLayout 是在 src/view/common 下的 BorderLayout.js 中自定義的類,其中 ht.Default.def(className, superClass, methods) 是 HT 中封裝的自定義類的函數,其中 className 為自定義類名, superClass 為要繼承的父類,methods 為方法和變數聲明,要使用這個方法要先在外部定義這個函數變數,通過 functionName.superClass.constructor.call(this) 方法繼承。BorderLayout 自定義類繼承了 ht.ui.drawable.BorderLayout 布局組件,此布局器將自身空間劃分為上、下、左、右、中間五個區域,每個區域可以放置一個子組件。為了能正常交互,重寫 getSplitterAt 函數將 splitterRect 的寬度修改為 10,以及為了調整左側 splitterCanvas 的尺寸,以便擋住子組件而重寫的 layoutSplitterCanvas 兩個方法:

let BorderLayout = function() { BorderLayout.superClass.constructor.call(this); this.setContinuous(true); this.setSplitterSize(0);};ht.Default.def(BorderLayout, ht.ui.BorderLayout, {// 自定義類 /** * splitter 寬度都為 0,為了能正常交互,重寫此函數將 splitterRect 的寬度修改為 10 * @override */ getSplitterAt: function (event) {// 獲取事件對象下分隔條所在的區域 var leftRect = this._leftSplitterRect, lp; if (leftRect) { leftRect = ht.Default.clone(leftRect); leftRect.width = 10; leftRect.x -= 5; if (event instanceof Event) lp = this.lp(event); else lp = event; if (ht.Default.containsPoint(leftRect, lp)) return left; } return BorderLayout.superClass.getSplitterAt.call(this, event); }, /** * 調整左側 splitterCanvas 的尺寸,以便擋住子組件 * @override */ layoutSplitterCanvas: function(canvas, x, y, width, height, region) { if (region === left) { canvas.style.pointerEvents = ; canvas.style.display = block; ht.Default.setCanvas(canvas, 10, height); canvas.style.left = this.getContentLeft() + this.tx() + x - 5 + px; canvas.style.top = this.getContentTop() + this.ty() + y + px; } else { BorderLayout.superClass.layoutSplitterCanvas.call(this, canvas, x, y, width, height, region); } }});export default BorderLayout;

左側欄

左側欄 sidebar,分為 8 個部分:頂部 logo、貨位統計表格、進度條、分割線、貨物表格、圖表、管理組、問題反饋按鈕等。

可以查看 src/view 下的 sidebar.js 文件,這個 js 文件中同樣載入了 src/view/common 下的TreeHoverBackgroundDrawable.js 和 ProgressBarSelectBarDrawable.js 中的 TreeHoverBackgroundDrawable 和 ProgressBarSelectBarDrawable 變數,以及 src/controller 下的 sidebar.js 中的 controller 變數:

import TreeHoverBackgroundDrawable from ./common/TreeHoverBackgroundDrawable.js;import ProgressBarSelectBarDrawable from ./common/ProgressBarSelectBarDrawable.js;import controller from ../controller/sidebar.js;

HT 封裝了一個 ht.ui.VBoxLayout 函數,用來將子組件放置在同一垂直列中,我們可以將左側欄要顯示的部分都放到這個組件中,這樣所有的部分都是以垂直列排布:

let vBoxLayout = new ht.ui.VBoxLayout();// 此布局器將子組件放置在同一垂直列中;

vBoxLayout.setBackground(#17191a);

頂部 logo 是根據在 Label 標籤上添加 icon 的方法來實現的,並將這個 topLabel 添加進垂直列 vBoxLayout 中:

let topLabel = new ht.ui.Label(); // 標籤組件topLabel.setText(Demo-logo);// 設置文字內容topLabel.setIcon(imgs/logo.json);// 設置圖標,可以是顏色或者圖片等topLabel.setIconWidth(41);topLabel.setIconHeight(37);topLabel.setTextFont(18px arial, sans-serif);topLabel.setTextColor(#fff);topLabel.setPreferredSize(1, 64);// 組件自身最合適的尺寸topLabel.setBackground(rgb(49,98,232));vBoxLayout.addView(topLabel, {// 將子組件加到容器中 width: match_parent// 填滿父容器 });

對於「貨位統計表格」,我們採用的是 HT 封裝的 TreeTableView 組件,以樹和表格的組合方式呈現 DataModel 中數據元素屬性及父子關係,並將這個「樹表」添加進垂直列 vBoxLayout 中:

let shelfTreeTable = new ht.ui.TreeTableView();// 樹表組件,以樹和表格的組合方式呈現 DataModel 中數據元素屬性及父子關係shelfTreeTable.setHoverBackgroundDrawable(new TreeHoverBackgroundDrawable(#1ceddf, 2));// 設置 hover 狀態下行選中背景的 Drawable 對象shelfTreeTable.setSelectBackgroundDrawable(new TreeHoverBackgroundDrawable(#1ceddf, 2));// 設置行選中背景的 Drawable 對象 參數為「背景shelfTreeTable.setBackground(null);shelfTreeTable.setIndent(20);// 設置不同層次的縮進值shelfTreeTable.setColumnLineVisible(false);// 設置列線是否可見shelfTreeTable.setRowLineVisible(false);shelfTreeTable.setExpandIcon(imgs/expand.json);// 設置展開圖標圖標,可以是顏色或者圖片等shelfTreeTable.setCollapseIcon(imgs/collapse.json);// 設置合併圖標圖標,可以是顏色或者圖片等shelfTreeTable.setPreferredSizeRowCountLimit();// 設置計算 preferredSize 時要限制的數據行數shelfTreeTable.setId(shelfTreeTable);vBoxLayout.addView(shelfTreeTable, { width: match_parent, height: wrap_content,// 組件自身首選高度 marginTop: 24, marginLeft: 4, marginRight: 4});

我們在設置「行選中」時背景傳入了一個 TreeHoverBackgroundDrawable 對象,這個對象是在 srcviewcommon 下的 TreeHoverBackgroundDrawable.js 文件中定義的,其中 ht.Default.def(className, superClass, methods) 是 HT 中封裝的自定義類的函數,其中 className 為自定義類名, superClass 為要繼承的父類,methods 為方法和變數聲明,要使用這個方法要先在外部定義這個函數變數,通過 functionName.superClass.constructor.call(this) 方法繼承。TreeHoverBackgroundDrawable 自定義類繼承了 ht.ui.drawable.Drawable 組件用於繪製組件背景、圖標等,只重寫了 draw 和 getSerializableProperties 兩個方法,我們在 draw 方法中重繪了 shelfTreeTable 的行選中背景色,並重載了 getSerializableProperties 序列化組件函數,並將 TreeHoverBackgroundDrawable 傳入的參數作為 map 中新添加的屬性:

let TreeHoverBackgroundDrawable = function(color, width) { TreeHoverBackgroundDrawable.superClass.constructor.call(this); this.setColor(color); this.setWidth(width);};ht.Default.def(TreeHoverBackgroundDrawable, ht.ui.drawable.Drawable, { ms_ac: [color, width], draw: function(x, y, width, height, data, view, dom) { var self = this, g = view.getRootContext(dom), color = self.getColor(); g.beginPath(); g.fillStyle = color; g.rect(x, y, self.getWidth(), height); g.fill(); }, getSerializableProperties: function() { var parentProperties = TreeHoverBackgroundDrawable.superClass.getSerializableProperties.call(this); return addMethod(parentProperties, { color: 1, width: 1 }); }});

記住要導出 TreeHoverBackgroundDrawable :

export default TreeHoverBackgroundDrawable;

HT 還封裝了非常好用的 ht.ui.ProgressBar 組件,可直接繪製進度條:

let progressBar = new ht.ui.ProgressBar();progressBar.setId(progressBar);progressBar.setBackground(#3b2a00);// 設置組件的背景,可以是顏色或者圖片等progressBar.setBar(rgba(0,0,0,0));// 設置進度條背景,可以是顏色或者圖片等progressBar.setPadding(5);progressBar.setSelectBarDrawable(new ProgressBarSelectBarDrawable(#c58348, #ffa866)); // 設置前景(即進度覆蓋區域)的 Drawable 對象,可以是顏色或者圖片等progressBar.setValue(40);// 設置當前進度值progressBar.setBorderRadius(0);vBoxLayout.addView(progressBar, { marginTop: 24, width: match_parent, height: 28, marginBottom: 24, marginLeft: 14, marginRight: 14});

我們在 設置「前景」的時候傳入了一個 ProgressBarSelectBarDrawable 對象,這個對象在 srcviewcommon 下的 ProgressBarSelectBarDrawable.js 中定義的。具體定義方法跟上面的 TreeHoverBackgroundDrawable 函數對象類似,這裡不再贅述。

分割線的製作最為簡單,只要將一個矩形的高度設置為 1 即可,我們用 ht.ui.View() 組件來製作:

let separator = new ht.ui.View();// 所有視圖組件的基類,所有可視化組件都必須從此類繼承separator.setBackground(#666);vBoxLayout.addView(separator, { width: match_parent, height: 1, marginLeft: 14, marginRight: 14, marginBottom: 24});

貨物表格的操作幾乎和貨位統計表格相同,這裡不再贅述。

我們將一個 json 的圖表文件當做圖片傳給圖表的組件容器作為背景,也能很輕鬆地操作:

let chartView = new ht.ui.View();chartView.setBackground(imgs/chart.json);vBoxLayout.addView(chartView, { width: 173, height: 179, align: center, marginBottom: 10});

管理組和頂部 logo 的定義方式類似,這裡不再贅述。

問題反饋按鈕,我們將這個部分用 HT 封裝的 ht.ui.Button 組件來製作,並將這個部分添加進垂直列 vBoxLayout 中:

let feedbackButton = new ht.ui.Button();// 按鈕類feedbackButton.setId(feedbackButton);feedbackButton.setText(問題反饋:service@hightopo.com);feedbackButton.setIcon(imgs/em.json);feedbackButton.setTextColor(#fff);feedbackButton.setHoverTextColor(shelfTreeTable.getHoverLabelColor());// 設置 hover 狀態下文字顏色feedbackButton.setActiveTextColor(feedbackButton.getHoverTextColor());// 設置 active 狀態下文字顏色feedbackButton.setIconWidth(16);feedbackButton.setIconHeight(16);feedbackButton.setIconTextGap(10);feedbackButton.setAlign(left);feedbackButton.setBackground(null);feedbackButton.setHoverBackground(null);feedbackButton.setActiveBackground(null);vBoxLayout.addView(feedbackButton, { width: match_parent, marginTop: 5, marginBottom: 10, marginLeft: 20});

交互

視圖部分做好了,在模塊化開發中,controller 就是做交互的部分,shelfTreeTable 貨位統計表格, cargoTreeTable 貨物表格, feedbackButton 問題反饋按鈕, progressBar 進度條四個部分的交互都是在在 src/controller 下的 sidebar.js 中定義的。通過 findViewById(id, recursive) 根據id查找子組件,recursive 表示是否遞歸查找。

shelfTreeTable 貨位統計表格的數據綁定傳輸方式與 cargoTreeTable 貨物表格類似,這裡我們只對 shelfTreeTable 貨位統計表格的數據綁定進行解析。shelfTreeTable 一共有三列,其中不同的部分只有「已用」和「剩餘」兩個部分,所以我們只要將這兩個部分進行數據綁定即可,先創建兩列:

let column = new ht.ui.Column();// 列數據,用於定義表格組件的列信息column.setName(used);// 設置數據元素名稱column.setAccessType(attr);// 在這裡 name 為 used,採用 getAttr(used) 和 setAttr(used, 98) 的方式存取 set/getAttr 簡寫為 acolumn.setWidth(65);column.setAlign(center);columnModel.add(column);column = new ht.ui.Column();column.setName(remain);column.setAccessType(attr);column.setWidth(65);column.setAlign(center);columnModel.add(column);

接著遍歷 json 文件,將 json 文件中對應的 used、remain以及 labelColors 通過 set/getAttr 或 簡寫 a 的方式進行數據綁定:

for (var i = 0; i < json.length; i++) { var row = json[i];// 獲取 json 中的屬性 var data = new ht.Data(); data.setIcon(row.icon);// 將 json 中的 icon 傳過來 data.setName(row.name); data.a(used, row.used); data.a(remain, row.remain); data.a(labelColors, row.colors); data.setIcon(row.icon); treeTable.dm().add(data);// 在樹表組件的數據模型中添加這個 data 節點 var children = row.children; if (children) { for (var j = 0; j < children.length; j++) { var child = children[j]; var childData = new ht.Data(); childData.setName(child.name); childData.setIcon(child.icon); childData.a(used, child.used); childData.a(remain, child.remain); childData.a(labelColors, child.colors); childData.setParent(data); treeTable.dm().add(childData); } }}

最後在 controller 函數對象中調用 這個函數:

initTreeTableDatas(shelfTreeTable, json);// json 為 ../model/shelf.json 傳入

progressBar 進度條的變化是通過設置定時器改變 progressBar 的 value 值來動態改變的:

setInterval(() => { if (progressBar.getValue() >= 100) { progressBar.setValue(0); } progressBar.setValue(progressBar.getValue() + 1);}, 50);feedbackButton 問題反饋按鈕通過增加 View 事件監聽器來監聽按鈕的點擊事件feedbackButton.addViewListener(e => { if (e.kind === click) {// HT 自定義的事件屬性,具體查看 http://hightopo.com/guide/guide/core/beginners/ht-beginners-guide.html window.location.href = "mailto:service@www.hightopo.com";// 當前頁面打開URL頁面 }});

右側容器splitLayout

直接用的分割組件 ht.ui.SplitLayout 進行分割布局:

let splitLayout = new ht.ui.SplitLayout();// 此布局器將自身空間劃分為上、下兩個區域或左、右兩個區域,每個區域可以放置一個子組件splitLayout.setSplitterVisible(false);splitLayout.setPositionType(absoluteFirst);splitLayout.setOrientation(v);

  • 右側頭部 header

這個 header 是從 src/view 下的 header.js 中獲取的對象,為 ht.ui.RelativeLayout 相對定位布局器,分為 5 個部分:searchField 搜索框、titleLabel 主標題、temperatureLabel1 溫度、humidityLabel1 濕度以及 airpressureLabel1 氣壓。

這裡我們沒有對「搜索框」 searchField 進行數據綁定,以及搜索的功能,這只是一個樣例,不涉及業務部分:

let searchField = new ht.ui.TextField();// 文本框組件searchField.setBorder(new ht.ui.border.LineBorder(1, #d8d8d8));// 在組件的畫布上繪製直線邊框searchField.setBorderRadius(12);searchField.setBackground(null);searchField.setIcon(imgs/search.json);searchField.setIconPosition(left);searchField.setPadding([2, 16, 2, 16]);searchField.setColor(rgb(138, 138, 138));searchField.setPlaceholder(Find everything...);searchField.getView().className = search;header.addView(searchField, { width: 180, marginLeft: 20, vAlign: middle});

對於 titleLabel 主標題比較簡單,和溫度、濕度以及氣壓類似,我就只說明一下主標題 titleLabel 的定義:

let titleLabel = new ht.ui.Label();// 標籤組件titleLabel.setId(title);titleLabel.setIcon(imgs/expand.json);titleLabel.setTextColor(rgb(138, 138, 138));titleLabel.setText(杭州倉庫);titleLabel.setHTextPosition(left);// 設置文字在水平方向相對於圖標的位置,默認值為 righttitleLabel.setIconTextGap(10);// 設置圖標和文字之間的間距titleLabel.setBorder(new ht.ui.border.IndividualLineBorder(0, 0, 3, 0, #3162e8))// 在組件的畫布上繪製直線邊框;與 LineBorder 不同的是,此邊框可以單獨繪製某一個或幾個方向的邊框titleLabel.setTextFont(16px arial);header.addView(titleLabel, { height: match_parent, width: wrap_content, align: center});

然後交互部分在 src/controller 下的 header.js 中做了右鍵點擊出現菜單欄以及單擊 titleLabel 的位置出現下拉菜單兩種交互,通過控制滑鼠的點擊事件來控制事件的交互:

let title, contextMenu;export default function controller (view) { title = view.findViewById(title); contextMenu = new ht.ui.ContextMenu();// 右鍵菜單組件 contextMenu.setLabelColor(rgb(138, 138, 138)); contextMenu.setHoverRowBackground(#3664e4); contextMenu.setItems([ { label: 北京倉庫 }, { label: 上海倉庫 }, { label: 廈門倉庫 } ]); contextMenu.addViewListener((e) => { if (e.kind === action) {// HT 自定義的事件類型 title.setText(e.item.label); } }); title.getView().addEventListener(mousedown, e => { if (contextMenu.isInDOM()) {// 判斷組件是否在 DOM 樹中 contextMenu.hide();// 隱藏菜單 document.removeEventListener(mousedown, handleWindowClick);// 移除mousedown監聽事件 } else {// 沒有右鍵點擊過 var items = contextMenu.getItems(); for (var i = 0; i < items.length; i++) { items[i].width = title.getWidth(); } let windowInfo = ht.Default.getWindowInfo(),// 獲取當前窗口left|top|width|height的參數信息 titleRect = title.getView().getBoundingClientRect(); contextMenu.show(windowInfo.left + titleRect.left, windowInfo.top + titleRect.top + titleRect.height); document.addEventListener(mousedown, handleWindowClick); } });}function handleWindowClick(e) { if (!contextMenu.getView().contains(e.target) && !title.getView().contains(e.target)) {// 判斷元素是否在數組中 contextMenu.hide(); document.removeEventListener(mousedown, handleWindowClick); }}

  • 右側下部分 RelativeLayout 相對布局器(相對於右側下部分最根層 div),包含中間顯示 3d 部分 graph3dView、雙擊貨櫃或貨物才會出現的 shelfPane、以及出現在右下角的圖表 chartPane,將這三部分添加進 RelativeLayout 相對布局容器:

let relativeLayout = new ht.ui.RelativeLayout();// 創建相對布局器relativeLayout.setId(contentRelative);relativeLayout.setBackground(#060811);var htView = new ht.ui.HTView(graph3dView);htView.setId(contentHTView);relativeLayout.addView(htView, {// 將 3d 組件添加進relativeLayout 相對布局器 width: match_parent, height: match_parent});relativeLayout.addView(shelfPane, {// 將雙擊出現的詳細信息 shelfPane 組件添加進relativeLayout 相對布局器 width: 220, height: wrap_content, align: right, marginRight: 30, marginTop: 30});relativeLayout.addView(chartPane, {// 將圖表 chartPane 組件添加進relativeLayout 相對布局器 width: 220, height: 200, align: right, vAlign: bottom, marginRight: 30, marginBottom: 30})

然後將右側相對布局器 relativeLayout 和右側頭部 header 添加進右側底部容器 splitLayout:

let splitLayout = new ht.ui.SplitLayout();splitLayout.setSplitterVisible(false);splitLayout.setPositionType(absoluteFirst);splitLayout.setOrientation(v);splitLayout.addView(header, { region: first// 指定組件所在的區域,可選值為:first|second});splitLayout.addView(relativeLayout, { region: second});再將左側部分的 sidebar 和右側部分的所有也就是 splitLayout 添加進整個底部容器 borderLayout再將底部容器添加進 html body 體中let borderLayout = new BorderLayout();borderLayout.setLeftWidth(250);borderLayout.addView(sidebar, { region: left,// 指定組件所在的區域,可選值為:top|right|bottom|left|center width: match_parent// 組件自身首選寬度 });borderLayout.addView(splitLayout, { region: center});borderLayout.addToDOM();// 將 borderLayout 添加進 body 體中

我們具體說說這個相對布局器內部包含的 3d 部分 graph3dView、雙擊貨櫃或貨物才會出現的 shelfPane、以及出現在右下角的圖表 chartPane。

3D 場景

從 srcview3d 文件夾中的 index.js 中獲取 graph3dView 的外部介面被 src/view 中的 index.js 調用:

import graph3dView from ./3d/index;

從這個 3d 場景中可以看到,我們需要「地板」、「牆面」、「貨架」、「叉車」、「貨物」以及 3d 場景。

在 3d 文件夾下的 index.js 中,我們從文件夾中導入所有需要的介面:

import {// 這裡導入的都是一些基礎數據 sceneWidth, sceneHeight, sceneTall, toShelfList, randomCargoType} from ./G.js;// 模擬數據介面import { stockinout,// 出入庫 initiate,// 初始化 inoutShelf// 上下架} from ./interfaces;import { Shelf } from ./shelf;// 貨架import { Floor } from ./floor;// 地板import { Wall } from ./wall;// 牆面import { Car } from ./car;// 叉車import { g3d } from ./g3d;// 3d場景import { getCargoById } from ./cargo;// 貨物g3d.js 文件中只設置了場景以及對部分事件的監聽g3d.mi((e) => {// 監聽事件 addInteractorListener const kind = e.kind; if (kind === doubleClickData) {// 雙擊圖元事件 let data = e.data;// 事件相關的數據元素 if (data instanceof Shelf) {// 如果是貨架 data.setTransparent(false); eventbus.fire({ type: cargoBlur });// 派發事件,依次調用所有的監聽器函數 } else { data = data.a(cargo); if (!data) return; data.transparent = false; eventbus.fire({ type: cargoFocus, data: data }); } for (let i = shelfList.length - 1; i >= 0; i--) {// 除了雙擊的圖元,其他的圖元都設置透明 const shelf = shelfList[i]; shelf.setTransparent(true, data); } return; } if (kind === doubleClickBackground) {// 雙擊背景事件 for (let i = shelfList.length - 1; i >= 0; i--) {// 雙擊背景,所有的圖元都不透明 const shelf = shelfList[i]; shelf.setTransparent(false); } eventbus.fire({ type: cargoBlur }); return; }});

我們在 G.js 中定義了一些基礎數據,其他引用的 js 中都會反覆調用這些變數,所以我們先來解析這個文件:

const sceneWidth = 1200;// 場景寬度const sceneHeight = 800;// 場景高度const sceneTall = 410;// 場景的深度const globalOpacity = 0.3;// 透明度const cargoTypes = {// 貨物類型,分為四種 cask: {// 木桶 name: bucket }, carton: {// 紙箱 name: carton }, woodenBox1: {// 木箱1 name: woodenBox1 }, woodenBox2: {// 木箱2 name: woodenBox2 }};

裡面有三個函數,分別是「貨架的 obj 分解」、「載入模型」、「隨機分配貨物的類型」:

function toShelfList(list) {// 將貨架的 obj 分解, const obj = {}; list.forEach((o) => {// 這邊的參數o具體內容可以查看 view/3d/interface.js const strs = o.cubeGeoId.split(-); let rs = obj[o.rackId]; if (!rs) { rs = obj[o.rackId] = []; } const ri = parseInt(strs[2].substr(1)) - 1; let ps = rs[ri]; if (!ps) { ps = rs[ri] = []; } let type = cask; if (o.inventoryType === Import) { while((type = randomCargoType()) === cask) {} } const pi = parseInt(strs[3].substr(1)) - 1; ps[pi] = { id: o.cubeGeoId, type: type }; }); return obj;}function loadObj(shape3d, fileName, cbFunc) {// 載入模型 const path = ./objs/ + fileName; ht.Default.loadObj(path + .obj, path + .mtl, { shape3d: shape3d, center: true, cube: true, finishFunc: cbFunc });}function randomCargoType() {// 隨機分配「貨物」的類型 const keys = Object.keys(cargoTypes); const i = Math.floor(Math.random() * keys.length); return keys[i];}

這個 3d 場景中還有不可缺少的「貨物」和「貨架」以及「叉車」,三者的定義方式類似,這裡只對「貨架」進行解釋。我們直接在「貨物」的 js 中引入底下的「托盤」的 js 文件,將它們看做一個整體:

import { Pallet } from ./pallet;import { cargoTypes, loadObj, globalOpacity} from ./G;

在 srcview3dcargo.js 文件中,定義了一個「貨物」類,這個類中聲明了很多方法,比較基礎,有需要的自己可以查看這個文件,這裡我不過多解釋。主要講一下如何載入這個「貨物」的 obj,我們在 G.js 文件中有定義一個 loadObj 函數,我們在代碼頂部也有引入,導入 obj 文件之後就在「貨物」的庫存增加這個「貨物」:

for (let type in cargoTypes) {// 遍歷 cargoTypes 數組, G.js 中定義的 const cargo = cargoTypes[type]; loadObj(type, cargo.name, (map, array, s3) => {// loadObj(shape3d, fileName, cbFunc) cbFunc 中的參數可以參考 obj 手冊 cargo.s3 = s3;// 將 cargo 的 s3 設置原始大小 updateCargoSize(); });}function updateCargoSize() { let c, obj; for (let i = cargoList.length - 1; i >= 0; i--) { c = cargoList[i]; obj = cargoTypes[c.type]; if (!obj.s3) continue; c.boxS3 = obj.s3; }}

還有就是界面上「貨物」的進出庫的動畫,主要用的方法是 HT 封裝的 ht.Default.startAnim 函數(HT for Web 動畫手冊),出的動畫與進的動畫類似,這裡不贅述:

// 貨物進in() { if (anim) {// 如果有值,就停止動畫 anim.stop(true); } this.x = this.basicX + moveDistance; this.opacity = 1; anim = ht.Default.startAnim({ duration: 1500, finishFunc: () => {// 動畫結束之後調用這個函數,將anim設置為空停止動畫 anim = null; }, action: (v, t) => { this.x = this.basicX + (1 - v) * moveDistance;// 改變x坐標,看起來像向前移動 } });}

牆和地板也是比較簡單的,簡單地繼承 ht.Node 和 ht.Shape,這裡以「牆」進行解釋,繼承之後直接在構造函數中進行屬性的設置即可:

class Wall extends ht.Shape {// 繼承 ht.Shape 類 constructor(points, segments, tall, thickness, elevation) { super(); this.setPoints(points);// 設置「點」 this.setSegments(segments);// 設置「點之間的連接方式」 this.setTall(tall);// 控制Node圖元在y軸的長度 this.setThickness(thickness);// 設置「厚度」 this.setElevation(elevation);// 控制Node圖元中心位置所在3D坐標系的y軸位置 this.s({ all.transparent: true,// 六面透明 all.opacity: 0.3,// 透明度為 0.3 all.reverse.flip: true,// 六面的反面顯示正面的內容 bottom.visible: false// 底面不可見 }); }}

floor、wall、shelf 以及 car 這四個類都準備完畢,只需要在 srcview3dindex.js 中 new 一個新的對象並加入到數據模型 dataModel 中即可,這裡只展示 car 「叉車」的初始化代碼:

// init Carconst car = new Car();car.addToDataModel(dm);

至於「貨物」,我們在這個 js 上是採用定時器調用 in 和 out 方法,這裡有一個模擬的資料庫 interfaces.js 文件,有需求的可以看一下,這裡我們只當數據來調用(進出庫和上下架類似,這裡只展示進出庫的設置方法):

// 輪訓掉用出入庫介面setInterval(() => { const obj = stockinout();// 出入庫 let type = cask; if (obj.inventoryType === Import) { while((type = randomCargoType()) === cask) {}// 如果為「貨物」類型為「木桶」 } car.cargoType = type; if (obj.inOutStatus === I)// 如果值為 「I」,則進庫 car.in(); else // 否則為「o」,出庫 car.out();}, 30000);

貨架

從 srcviewcommon 文件夾中的 shelfPane.js 中獲取 graph3dView 的外部介面被 src/view 中的 index.js 調用:

import shelfPane from ./common/shelfPane.js;

shelfPane 是基於 Pane 類的,在 shelfPane.js 文件中引入這個類和事件派發器:

import Pane from ./Pane.js;import eventbus from ../../controller/eventbus;Pane 類繼承於 HT 封裝的 ht.ui.TabLayout 並做了一些特定的屬性設置class Pane extends ht.ui.TabLayout { constructor() { super(); this.setBorder(new ht.ui.border.LineBorder(1, rgb(150,150,150)));// 設置組件的邊框 this.setTabHeaderBackground(null);// 設置標籤行背景,可以是顏色或者圖片等 this.setHoverTabBackground(null);// 設置 Hover 狀態下的標籤背景,可以是顏色或者圖片等 this.setActiveTabBackground(null);// 設置 Active 狀態下的標籤背景,可以是顏色或者圖片等 this.setTitleColor(rgb(184,184,184));// 設置正常狀態下標籤文字的顏色 this.setActiveTitleColor(rgb(255,255,255));// 設置 Active 狀態下標籤文字的顏色 this.setTabHeaderLineSize(0);// 設置標籤行分割線寬度 this.setMovable(false);// 設置標籤是否可拖拽調整位置,默認為 true this.setTabHeaderBackground(#1c258c);// 設置標籤行背景,可以是顏色或者圖片等 this.setTabGap(0);// 設置標籤之間的距離 } getTabWidth(child) {// 獲取指定子組件的標籤寬度 const children = this.getChildren(), size = children.size(); if (size === 0) { return this.getContentWidth();// 獲取內容寬度,即組件寬度減去邊框寬度和左右內邊距寬度 } else { return this.getContentWidth() / size; } } drawTab(g, child, x, y, w, h) {// 繪製標籤 const children = this.getChildren(),// 獲取子組件列表 size = children.size(), color = this.getCurrentTitleColor(child),// 根據參數子組件的狀態(normal、hover、active、move),獲取標籤文字顏色 font = this.getTitleFont(child),// 獲取標籤文字字體 title = this.getTitle(child);// 獲取指定子組件的標籤文本 if (size === 1) { ht.Default.drawText(g, title, font, color, x, y, w, h, left);// 繪製文字 } else { ht.Default.drawText(g, title, font, color, x, y, w, h, center); } if (children.indexOf(child) < size - 1) { g.beginPath();// 開始繪製 g.rect(x + w - 1, y + 4, 1, h - 8); g.fillStyle = rgb(150,150,150); g.fill(); } } show() { this.setVisible(true);// 設置組件是否可見 } hide() { this.setVisible(false); }}

我們這個例子中的「信息」列表是一個表格組件,HT 通過 ht.ui.TableLayout 函數定義一個表格,然後通過 ht.ui.TableRow 向表格中添加行,這個例子中的「備註」、「編號」、「來源」、「入庫」、「發往」以及「出庫」都是文本框,這裡拿「備註」作為舉例:

let tableLayout = new ht.ui.TableLayout();// 此布局器將自身空間按照行列數劃分為 row * column 個單元格tableLayout.setColumnPreferredWidth(0, 45);// 設置列首選寬度tableLayout.setColumnWeight(0, 0);// 設置列寬度權重;如果布局器的總寬度大於所有列的首選寬度之和,那麼剩餘的寬度就根據權重分配tableLayout.setColumnPreferredWidth(1, 150);tableLayout.setPadding(8);// 設置組件內邊距,參數如果是數字,說明四邊使用相同的內邊距;如果是數組,則格式為:[上邊距, 右邊距, 下邊距, 左邊距]// 備註var tableRow1 = new ht.ui.TableRow();// TableLayout 中的一行子組件;var label = new ht.ui.Label();// 標籤組件label.setText(備註);// 設置文字內容label.setAlign(left);// 設置文字和圖標在按鈕水平方向的整體對齊方式,默認為 centerlabel.setTextColor(rgb(255,255,255));// 設置文字顏色var textField = new ht.ui.TextField();// 文本框組件textField.setFormDataName(remark);// 設置組件在表單中的名稱textField.setBackground(null);// 設置組件的背景,可以是顏色或者圖片等;此值最終會被轉換為 Drawable 對象textField.setBorderRadius(0);// 設置 CSS 邊框圓角textField.setColor(rgb(138,138,138));// 設置文字顏色textField.setPlaceholder();// 設置輸入提示textField.setBorder(new ht.ui.border.IndividualLineBorder(0, 0, 1, 0, rgb(138,138,138)));// 設置組件的邊框tableRow1.addView(label);// 添加子組件tableRow1.addView(textField);tableLayout.addView(tableRow1);// 將子組件加到容器中

「歸類」和「模型」類似,都是下拉框,我們用 HT 封裝的 ht.ui.ComboBox 組合框組件,跟 ht.ui.TextField 也是異曲同工,只是具體操作不同而已,HT 這樣做使用上更簡便更容易上手,這裡我們以「模型」進行解析,在設置「下拉數據」的時候我們利用了 HT 中的數據綁定:

// 模型var tableRow4 = new ht.ui.TableRow();label = new ht.ui.Label();label.setText(模型);label.setAlign(left);label.setTextColor(rgb(255,255,255));var comboBox = new ht.ui.ComboBox();comboBox.setFormDataName(model);comboBox.setBackground(null);comboBox.setColor(rgb(232,143,49));comboBox.setDatas([// 設置下拉數據數組 { label: 紙箱, value: carton }, { label: 木箱1, value: woodenBox1 }, { label: 木箱2, value: woodenBox2 }, { label: 木桶, value: cask }]); comboBox.setIcon(imgs/combobox_icon.json);comboBox.setHoverIcon(imgs/combobox_icon_hover.json);comboBox.setActiveIcon(imgs/combobox_icon_hover.json);comboBox.setBorderRadius(0);// 設置 CSS 邊框圓角comboBox.setBorder(new ht.ui.border.IndividualLineBorder(0, 0, 1, 0, rgb(138,138,138)));tableRow4.addView(label);tableRow4.addView(comboBox);tableLayout.addView(tableRow4);

最後一個「染色」,HT 封裝了 ht.ui.ColorPicker 顏色選擇器組件,組件從 ht.ui.ComboBox 繼承並使用 ht.ui.ColorDropDown 作為下拉模板,跟上面的下拉列表很類似,只是下拉的模板變了而已:

// 染色var tableRow9 = new ht.ui.TableRow();label = new ht.ui.Label();label.setText(染色);label.setAlign(left);label.setTextColor(rgb(255,255,255));var comboBox = new ht.ui.ColorPicker();// 顏色選擇器組件comboBox.setFormDataName(blend);// 設置組件在表單中的名稱comboBox.getView().className = content_colorpicker;comboBox.setBackground(null);comboBox.setPreviewBackground(null);// 設置預覽背景;可以是顏色或者圖片等comboBox.getInput().style.visibility = visible;// 獲取組件內部的 input 框的 style 樣式comboBox.setReadOnly(true);// 設置只讀comboBox.setColor(rgba(0,0,0,0));comboBox.setPlaceholder(修改貨箱顏色);comboBox.setIcon(imgs/combobox_icon.json);comboBox.setHoverIcon(imgs/combobox_icon_hover.json);comboBox.setActiveIcon(imgs/combobox_icon_hover.json);comboBox.setBorderRadius(0);comboBox.setBorder(new ht.ui.border.IndividualLineBorder(0, 0, 1, 0, rgb(138,138,138)));comboBox.setInstant(true);// 設置即時模式;在這種模式下,每輸入一個字元 value 屬性變化事件就會立即被派發,否則只有失去焦點或敲回車時才被派發tableRow9.addView(label);tableRow9.addView(comboBox);tableLayout.addView(tableRow9);

最後通過 ht.ui.Form 組件的 addChangeListener 事件監聽函數監聽 JSON 整體變化事件和 JSON 中單條數據變化事件,這兩種事件的解釋如下圖:

具體監聽方法如下:

form.addChangeListener((e) => { const cargo = form.__cargo__; if (e.kind === formDataValueChange) {// JSON 中單條數據值變化事件 const name = e.name; let value = e.newValue; if (name === blend) { if (value && value.startsWith(rgba)) { const li = value.lastIndexOf(,); value = rgb + value.substring(value.indexOf((), li) + ); } } cargo.setValue(name, value); }});

然後通過 HT 封裝的事件派發器 ht.Notifier 將界面中不同區域的組件之間通過事件派發進行交互,根據不同的事件類型進行不同的動作:

eventbus.add((e) => {// 增加監聽器 事件匯流排;界面中不同區域的組件之間通過事件派發進行交互 if (e.type === cargoFocus) { shelfPane.show(); const cargo = e.data; form.__cargo__ = cargo; const json = form.getJSON();// 獲取由表單組件的名稱和值組裝成的 JSON 數據 for (let k in json) { form.setItem(k, cargo.getValue(k)); } return; } if (e.type === cargoBlur) { shelfPane.hide(); return; }});

圖表

從 srcviewcommon 文件夾中的 chartPane.js 中獲取 graph3dView 的外部介面被 src/view 中的 index.js 調用:

import chartPane from ./common/chartPane.js;

chartPane 和 shelfPane 類似,都是 Pane 類的對象,屬性也類似,不同的是內容。因為今天展示的只是一個 Demo,我們並沒有做過多的關於圖表插件的處理,所以這裡就用圖片來代替動態圖表,不過就算想做也是很容易的事,HT 官網上有更多有趣的例子!

回到正題,chartPane 圖表面板的實現非常容易,將內部的子組件設置背景圖片再添加進 chartPane 圖表面板中即可:

import Pane from ./Pane.js;var chartPane = new Pane();var view1 = new ht.ui.View();view1.setBackgroundDrawable(new ht.ui.drawable.ImageDrawable(imgs/chart.png, fill));// 設置組件的背景 Drawable 對象;組件渲染時優先使用此 Drawable 對象,如果為空,再用 background 轉換var view2 = new ht.ui.View();view2.setBackgroundDrawable(new ht.ui.drawable.ImageDrawable(imgs/chart.png, fill));chartPane.getView().style.background = rgba(18,28,64,0.60);// 設置背景顏色chartPane.addView(view1, {// 將子組件加到容器中 title: 其他圖表});chartPane.addView(view2, { title: 庫存負載});chartPane.setActiveView(view2);// 設置選中的子組件

整個例子解析完畢,有興趣的小夥伴可以去 HT 官網(hightopo.com/)上自習查閱資料,好好品味,一定會發現更大的世界。

hightopo.com/demo/large

總結

一直是世界「製造工廠」的中國製造業,面臨著前所未有的挑戰,一方面貿易戰後中國會更多地進口,會加大對世界的開放,更多「特斯拉」會進入中國,給本土製造業帶來威脅;另一方面,中國製造一直面臨的產能和外貿過剩問題也需要解決,抓住國內消費升級的趨勢,走出口轉內銷的路就成為一個必然選擇,要走好這條路同樣離不開智造智造。

智能製造的興起、貿易環境的變局,讓中國製造業轉型升級成為燃眉之急。


推薦閱讀:

錦州銀行|擁抱新技術,建設優秀人才梯隊
如何用三個步驟玩轉「互聯網+」?
胡健: 讓胡桃wootop的「溫度」熨帖到用戶足夠的滿意度 | 陳子涵
那些被天朝美食養肥的吃貨們,在國外怎麼過?

TAG:HTML5 | 3D | 互聯網+ |