基於 WEB 的 WMS 3D 可視化管理系統
前言
首先介紹一下什么是WMS。WMS是倉庫管理系統(Warehouse Management System) 的縮寫,倉庫管理系統是通過入庫業務、出庫業務、倉庫調撥、庫存調撥和虛倉管理等功能,對批次管理、物料對應、庫存盤點、質檢管理、虛倉管理和即時庫存管理等功能綜合運用的管理系統,有效控制並跟蹤倉庫業務的物流和成本管理全過程,實現或完善的企業倉儲信息管理。該系統可以獨立執行庫存操作,也可與其他系統的單據和憑證等結合使用,可為企業提供更為完整企業物流管理流程和財務管理信息。
目前主流的 WMS 倉庫管理系統大都采用了 B/S 模式,但數據可視化技術上仍采用的是傳統圖表顯示方式。本文從數據可視化的角度介紹了一種基於 WEB 的 3D 可視化實現方案,底層基於標准的 HTML5 WebGL 技術,以 3D 的方式顯示倉庫立體場景,包括貨架、貨物、堆垛機、穿梭車、輸送機等。相對於傳統圖表顯示方式,三維的倉庫管理可視化顯示方式,顯得更加直觀和立體化,無論是用戶體驗還是產品質量都得到了巨大提升。
一、WebGL 介紹以及 3D 引擎的選擇
WebGL(全寫Web Graphics Library)是一種3D繪圖協議,這種繪圖技術標准允許把JavaScript和 OpenGL ES 2.0 結合在一起,通過增加 OpenGL ES 2. 0的一個 JavaScript 綁定,WebGL 可以為HTML5 Canvas 提供硬件 3D 加速渲染,這樣Web開發人員就可以借助系統顯卡來在瀏覽器里更流暢地展示3D場景和模型了,還能創建復雜的導航和數據視覺化。顯然,WebGL 技術標准免去了開發網頁專用渲染插件的麻煩,可被用於創建具有復雜3D結構的網站頁面,甚至可以用來設計 3D 網頁游戲等等。
由於 WebGL 是一種偏底層的技術,為了降低開發難度和節省開發成本,不建議直接基於 WebGL進行開發。目前業內大都采用基於 WebGL 實現的 3D 引擎進行開發。Web 3D 引擎比較多,很多是面向不同行業和不同的應用場景的,下面我們介紹幾個常見的且有代表性的 3D 引擎,並選擇一個適合來用來構建 WMS 3D 可視化倉庫管理系統。
1. Three.js
Three.js 是純渲染引擎,而且代碼易讀,容易作為學習WebGL、3D圖形、3D數學應用的平台,也可以做中小型的重表現的Web項目。但如果要做中大型項目,尤其是多種媒體混雜的或者是游戲項目VR體驗項目,Three.js必須要配合更多擴展庫才能完成。
2. Babylon.js
Babylon.js 是微軟發布的開源的 Web 3D 引擎。最初設計作為一個Silverlight游戲引擎,Babylon.js 的維護傾向於基於 Web 的游戲開發與碰撞檢測和抗鋸齒等特性。在其官網上可以看到很多例子:
http://www.babylonjs.com/
。
3.
HT for Web
HT for Web 是基於
HTML5標准的企業應用圖形界面一站式解決方案,其包含通用組件、拓撲組件和3D渲染引擎等豐富的圖形界面開發類庫。雖然 HT for Web 是商業軟件但其提供的一站式解決方案可以極大縮短產品開發周期、減少研發成本、補齊我們在 Web 圖形界面可視化技術上的短板。
我們選擇的 3D 引擎是 HT for Web,雖然需要一定的授權費,但總體上來看是有價值的,我們在很短的時間內就可以開發出一套定制化的 WMS 3D 可視化倉庫管理系統。由於是商用軟件,對方提供了很好的技術支持,官網有完善的文檔手冊,開發包的使用也很容易上手。
二、功能實現
WMS 數據可視化主要包括以下幾部分功能:
1. 狀態管理
用於顯示WMS通訊狀態、堆垛機狀態,包括是否故障、通訊狀態、故障信息。
顯示狀態面板只需要引用 HT 的圖紙文件:
1 const g2d = new ht.graph.GraphView() 2 g2d.setPannable(false) 3 g2d.setRectSelectable(false) 4 g2d.handleScroll = function () {} 5 g2d.setScrollBarVisible(false) 6 7 ht.Default.xhrLoad('displays/狀態面板.json', function (json) { 8 g2d.dm().deserialize(json) 9 })
2. 任務管理
顯示當前出庫入庫任務列表
出庫入庫任務列表也可以用 HT 圖紙進行顯示:
1 const g2d = new ht.graph.GraphView() 2 3 g2d.setPannable(false) 4 g2d.setRectSelectable(false) 5 g2d.handleScroll = function () {} 6 g2d.setScrollBarVisible(false) 7 ht.Default.xhrLoad('displays/任務列表.json', function (json) { 8 g2d.dm().deserialize(json) 9 })
3. 故障管理
顯示當前的故障信息列表。
故障信息頁面為 HT 圖紙,代碼實現如下:
1 const g2d = new ht.graph.GraphView() 2 g2d.setPannable(false) 3 g2d.setRectSelectable(false) 4 g2d.handleScroll = function () {} 5 g2d.setScrollBarVisible(false) 6 7 ht.Default.xhrLoad('displays/故障信息.json', function (json) { 8 g2d.dm().deserialize(json); 9 });
4. 單機管理
提前信息后WMS實現貨物入庫或出庫。
入庫邏輯和出庫邏輯需要分別實現,整個過程涉及貨物在輸送出上的移動動畫、堆垛機的移動動畫、堆垛機的取貨放貨動畫。
貨物入庫核心代碼:
1 // 貨物入庫 2 function goodsIn(code) { 3 var good = dataModel.getDataByTag(code) 4 if (!good) { 5 console.warn('貨物編號不存在:', code) 6 return 7 } 8 ////////// 入庫口移動至輸入機 ////////////// 9 10 var row = good.a('row') 11 var col = good.a('col') 12 var floor = good.a('floor') 13 14 if (col <= colSize / 2) { // 左側 15 let goodP3 = dataModel.getDataByTag('入口1').p3() 16 goodP3[1] = floorBaseElevation 17 good.p3(goodP3) 18 } else { // 右側 19 let goodP3 = dataModel.getDataByTag('入口2').p3() 20 goodP3[1] = floorBaseElevation 21 good.p3(goodP3) 22 } 23 good.s('3d.visible', true) 24 good.setHost(null) 25 26 if (col <= colSize / 2) { // 左側 27 let refer = dataModel.getDataByTag('LeftFront') 28 moveZTo(good, refer.getY(), null, () => { 29 moveXTo(good, refer.getX(), null, () => { // 左移 30 // 后移至貨架水平位置 31 let targetY = null 32 if (Math.floor(row % 2) === 0) { // 偶數列 33 targetY = good.a('p3')[2] + 300 34 } else { 35 targetY = good.a('p3')[2] 36 } 37 moveZTo(good, targetY, null, () => { 38 // 右移至貨架邊緣 39 moveXTo(good, dataModel.getDataByTag('升降機L' + row + ':底座').getX(), null, () => { 40 // 離開輸送機移動至貨架 41 goodToShelve(good) 42 }) 43 }) 44 }) 45 }) 46 47 } else { // 右側 48 let refer = dataModel.getDataByTag('RightFront') 49 moveZTo(good, refer.getY(), null, () => { 50 moveXTo(good, refer.getX(), null, () => { // 右移 51 // 后移至貨架水平位置 52 let targetY = null 53 if (Math.floor(row % 2) === 0) { // 偶數列 54 targetY = good.a('p3')[2] + 300 55 } else { 56 targetY = good.a('p3')[2] 57 } 58 moveZTo(good, targetY, null, () => { 59 // 左移至貨架邊緣 60 moveXTo(good, dataModel.getDataByTag('升降機R' + row + ':底座').getX(), null, () => { 61 // 離開輸送機移動至貨架 62 goodToShelve(good) 63 }) 64 }) 65 }) 66 }) 67 } 68 }
貨物出庫核心代碼:
1 // 貨物出庫 2 function goodsOut(code) { 3 var good = dataModel.getDataByTag(code) 4 if (!good) { 5 console.warn('貨物編號不存在:', code) 6 return 7 } 8 9 var row = good.a('row') 10 var col = good.a('col') 11 var floor = good.a('floor') 12 13 let elevatorRow = parseInt((row + 1) / 2) 14 let isLeft = col <= (colSize / 2) 15 let elevator = isLeft ? dataModel.getDataByTag("升降機L" + elevatorRow) : dataModel.getDataByTag("升降機R" + elevatorRow) 16 17 let elevatorX = elevator.getX() 18 let x = (good.getX() - elevatorX) 19 // 水平移動 20 ht.Default.startAnim({ 21 duration: Math.abs(col - elevator.a('col')) * animationUnit, // 動畫周期毫秒數,默認采用`ht.Default.animDuration` 22 action: function (v, t) { 23 elevator.setX(elevatorX + x * v) 24 }, 25 finishFunc: function () { 26 elevator.a('col', col) 27 28 // 底座垂直移動 29 let base = dataModel.getDataByTag(elevator.getTag() + ":底座") 30 if (floor > 1) { 31 baseUp(base, good, floor, true, false) 32 } else { 33 // 取貨,出貨 34 startHandAnimation(base, good, floor, true, false) 35 } 36 } 37 }); 38 }
堆垛機上升動畫實現:
1 function elevatorIn(elevator, good) { 2 console.log('elevatorIn') 3 var row = good.a('row') 4 var col = good.a('col') 5 var floor = good.a('floor') 6 7 let elevatorX = elevator.getX() 8 let goodP3 = good.a('p3') 9 let x = (goodP3[0] - elevatorX) 10 // 水平移動 11 ht.Default.startAnim({ 12 duration: Math.abs(col - elevator.a('col')) * animationUnit, // 動畫周期毫秒數,默認采用`ht.Default.animDuration` 13 action: function (v, t) { 14 elevator.setX(elevatorX + x * v) 15 }, 16 finishFunc: function () { 17 elevator.a('col', col) 18 19 // 底座垂直移動 20 let base = dataModel.getDataByTag(elevator.getTag() + ":底座") 21 if (floor > 1) { 22 baseUp(base, good, floor, false, true) 23 } else { 24 // 送貨 25 startHandAnimation(base, good, floor, false, true) 26 } 27 } 28 }); 29 }
堆垛機動畫:
1 // 堆垛機出貨 2 function elevatorOut(elevator, good, goodIn) { 3 console.log('elevatorOut') 4 let elevatorX = elevator.getX() 5 let isLeft = elevator.getTag().startsWith('升降機L') 6 let start = isLeft ? LeftElevatorX : RightElevatorX 7 let xOffset = (start - elevatorX) 8 9 let t = isLeft ? Math.abs(elevator.a('col')) : Math.abs(colSize - elevator.a('col') + 1) 10 // 水平移動 11 ht.Default.startAnim({ 12 duration: t * animationUnit, // 動畫周期毫秒數,默認采用`ht.Default.animDuration` 13 action: function (v, t) { 14 elevator.setX(elevatorX + xOffset * v) 15 }, 16 finishFunc: function () { 17 elevator.a('col', isLeft ? 0 : (colSize + 1)) 18 if (!goodIn) { 19 startHandAnimation(dataModel.getDataByTag(elevator.getTag() + ":底座"), good, 1, false, goodIn) 20 } 21 } 22 }) 23 } 24 25 // 堆垛機取貨 26 function elevatorIn(elevator, good) { 27 console.log('elevatorIn') 28 var row = good.a('row') 29 var col = good.a('col') 30 var floor = good.a('floor') 31 32 let elevatorX = elevator.getX() 33 let goodP3 = good.a('p3') 34 let x = (goodP3[0] - elevatorX) 35 // 水平移動 36 ht.Default.startAnim({ 37 duration: Math.abs(col - elevator.a('col')) * animationUnit, // 動畫周期毫秒數,默認采用`ht.Default.animDuration` 38 action: function (v, t) { 39 elevator.setX(elevatorX + x * v) 40 }, 41 finishFunc: function () { 42 elevator.a('col', col) 43 44 // 底座垂直移動 45 let base = dataModel.getDataByTag(elevator.getTag() + ":底座") 46 if (floor > 1) { 47 baseUp(base, good, floor, false, true) 48 } else { 49 // 送貨 50 startHandAnimation(base, good, floor, false, true) 51 } 52 } 53 }); 54 }
堆垛機底座和抓手動畫:
1 // 抓手動畫 2 function startHandAnimation(baseNode, goodNode, floor, pick, goodIn) { 3 console.log('startHandAnimation:', floor, pick, goodIn) 4 let elevator = baseNode.getParent() 5 // 抓手移動的方向 6 let isBack = goodNode.a('row') === elevator.a('row') * 2 7 baseNode.eachChild(hand => { 8 var z = hand.getY() 9 // 抓手動畫 10 ht.Default.startAnim({ 11 duration: 4000, // 動畫周期毫秒數,默認采用`ht.Default.animDuration` 12 easing: function (t) { 13 if (t < 0.5) { 14 return t * 2 15 } else { 16 return (1 - t) * 2 17 } 18 }, 19 action: function (v, t) { 20 if (t >= 0.5) { 21 if (pick) { 22 goodNode.setHost(hand) 23 } else { 24 goodNode.setHost(null) 25 } 26 } 27 if (goodIn) { 28 if (pick) { // 取貨 29 hand.setY(z + 150 * v) 30 } else { // 放貨 31 if (isBack) { 32 hand.setY(z - 150 * v) 33 } else { 34 hand.setY(z + 150 * v) 35 } 36 } 37 } else { 38 if (pick) { // 取貨 39 if (isBack) { 40 hand.setY(z - 150 * v) 41 } else { 42 hand.setY(z + 150 * v) 43 } 44 } else { // 放貨 45 hand.setY(z - 150 * v) 46 } 47 } 48 }, 49 finishFunc: function () { 50 if (baseNode.a('floor') > 1) { 51 baseDown(baseNode, goodNode, floor, pick, goodIn) 52 } else { 53 if (elevator.a('col') === 0 || elevator.a('col') === colSize + 1) { 54 if (goodIn) { // 入庫: 已完成取貨動作, 升降機進入貨架 55 elevatorIn(elevator, goodNode) 56 } else { // 出庫:已將貨物放置到輸送機 57 // 移動到小車位置 58 startGoodOutAnimation(goodNode) 59 } 60 } else { // 將升降機移到貨架外 61 elevatorOut(elevator, goodNode, goodIn) 62 } 63 } 64 } 65 }); 66 }) 67 } 68 69 // 底座上升 70 function baseUp(baseNode, goodNode, floor, pick, goodIn) { 71 console.log('底座上升:', baseNode.getTag()) 72 73 var baseElevation = baseNode.getElevation() 74 75 let goodP3 = goodNode.a('p3') 76 var elevationOffset = (goodP3[1] - baseElevation) 77 // 上升 78 ht.Default.startAnim({ 79 duration: (floor - 1) * animationUnit, 80 action: function (v, t) { 81 baseNode.setElevation(baseElevation + elevationOffset * v) 82 }, 83 finishFunc: function () { 84 baseNode.a('floor', floor) 85 startHandAnimation(baseNode, goodNode, floor, pick, goodIn) 86 } 87 }); 88 }
5. 主3D場景
以 3D 的方式顯示倉庫立體場景,包括貨架、貨物、堆垛機、穿梭車、輸送機等。支持常用視角切換,提供側視、俯視、正視、斜視。當選中某個貨物時。
視角切換圖標是基於 HT for Web 交互功能定制的圖標:
1 const g2d = new ht.graph.GraphView() 2 g2d.setPannable(false) 3 g2d.setRectSelectable(false) 4 g2d.handleScroll = function () {} 5 6 ht.Default.xhrLoad('displays/視角切換.json', function (json) { 7 g2d.dm().deserialize(json); 8 }); 9 10 g2d.lookAtFront = function () { 11 eventbus.trigger('g3d.lookAtFront') 12 } 13 g2d.lookAtLean = function () { 14 eventbus.trigger('g3d.lookAtLean') 15 } 16 g2d.lookAtLeft = function () { 17 eventbus.trigger('g3d.lookAtLeft') 18 } 19 g2d.lookAtTop = function () { 20 eventbus.trigger('g3d.lookAtTop') 21 }
可顯示貨物的詳細信息(托盤號、貨位、批號、物料代碼、物料名稱、單位、數量、備注、堆垛機號、質量狀態):
借助 HT for Web 的數據驅動模型以及動畫API,可以很容易地控制貨物出庫出庫動作,並與后台數據綁定。可以模擬堆垛機入庫取貨,貨物在輸送機上移動並出庫,貨物經過檢測門入庫等動畫效果。
