基于 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,可以很容易地控制货物出库出库动作,并与后台数据绑定。可以模拟堆垛机入库取货,货物在输送机上移动并出库,货物经过检测门入库等动画效果。