基于 WEB 的 WMS 3D 可视化管理系统

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

在线演示地址:http://www.hightopo.com/demo/wms/index.html

相关文章