You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
18 KiB
18 KiB
物流模型总体介绍
基本定义
物流单元大纲
-
点 point
- 辅助定位点 point
- 决策点 decision_point
- 扫码器 bcr
- 站点 station_point
-
线 line
-
输送线 conveyor
-
行走路径 moveline
-
辅助测量线 measure
-
弧线类型
- 直线 line
- 贝塞尔曲线 bessel
- 圆弧线 curved
-
-
存储 store
- 暂存区 queue
- 地堆区 ground_rack
- 常规货架 rack
- 立库货架 asrs_rack
- 密集库货架 flash_rack
- 多穿库货架 shuttle_rack
- 层间线 pd
-
任务执行器 executer
- 堆垛机 stacker
- 两向穿梭车 laser
- 四向穿梭车 flash
- 穿梭板 flash_tp
- 货物提升机 life
- 车提升机 flash_life
- 叉车 forklift
- 侧叉式AGV ptr
- 潜伏式AGV agv
- 背篓式AGV CTU
- 人工 people
- 机械手 robotic_arm
- 碟盘机 stacking
- 装卸塔 dump_tower
- 加工台 station
- 电子标签 tag
-
流动单元 flow_item
- box 纸箱
- tote 周转箱
- pallet 托盘
-
辅助 other
- 发生器 source
- 消失器 sink
- 任务分配器 dispatcher
- 文本 text
- 图片 image
- 区域 plane
物流世界
一个物流仓库, 就是一个世界 他有自己的项目定义, 楼层, 围墙, 柱子, 其他数据, 个性化脚本等等 每次对建模文件打开的时候, 因为性能问题, 一次只会读取一个楼层, 或者一个水平横截面. 因此, 一个 floor 就是对应一个 THREE.Scene
/**
* 物流世界模型
*/
export default class WorldModel {
/**
* 所有楼层 / 提升机横截面的目录
*/
allLevels = [
{
value: 'F', label: '仓库楼层',
children: [
{ value: '-f1', label: '地下室 (-f1)' },
{ value: 'f1', label: '一楼 (f1)' },
{ value: 'f2', label: '二楼 (f2)' },
{ value: 'OUT', label: '外场 (OUT)' },
{ value: 'fe', label: '楼层电梯 (fe)' }
]
},
{
value: 'M', label: '密集库区域',
children: [
{ value: 'm1', label: 'M1 (m1)' },
{ value: 'm2', label: 'M2 (m2)' },
{ value: 'm3', label: 'M3 (m3)' },
{ value: 'm4', label: 'M4 (m4)' },
{ value: 'me', label: '提升机 (me)' }
]
},
]
// 载入某个楼层
async loadFloor(floorId: string): Promise<ItemJson[]>
}
物流控制中心系统,基于 ThreeJS 和 Vue3 开发.
他的主要作用就是通过Web浏览器, 建立一个自动化立体仓库的物流模型
单元点 ItemJson
/**
* 物体单元(点)
*/
export interface ItemJson {
/**
* 对应 three.js 中的 uuid, 物体ID, 唯一标识, 需保证唯一, 有方法可以进行快速的 O(1) 查找
*/
id?: string
/**
* 物体名称, 显示用, 最后初始化到 three.js 的 name 中, 可以不设置, 可以不唯一, 但他的查找速度是 O(N)
*/
name?: string
/**
* "点"的物体单元类型, 最终对应到 measure / conveyor / task 等不同的单元处理逻辑中
*/
t: string
/**
* 变换矩阵, 3x3矩阵, 采用Y轴向上为正, X轴向右, Z轴向前的右手坐标系
*/
tf: [
/**
* 平移向量 position, 三维坐标
*/
[number, number, number],
/**
* 旋转向量 rotation, 表示绕Y轴旋转的角度, 单位为度。对应 three.js 应进行"角度"转"弧度"的换算
*/
[number, number, number],
/**
* 缩放向量 scale, 三维缩放比例
*/
[number, number, number],
]
/**
* 用户数据, 可自定义, 一般用在 three.js 的 userData 中
*/
dt: {
/**
* 标签名称, 显示用, 最后初始化到 three.js 的 userData.label 中, 最终应该如何渲染, 每个单元类型有自己的逻辑, 取决于物流单元类型t的 renderer 逻辑
*/
label?: string
/**
* 颜色, 最后初始化到 three.js 的 userData.color 中, 最终颜色应该如何渲染, 每个单元类型有自己的逻辑, 取决于物流单元类型t的 renderer 逻辑
*/
color?: string
/**
* S连线(又称逻辑连线), 与其他点之间的无方向性关联, 关系的起点需要在他的 dt.center[] 数组中添加目标点的id, 关系的终点需要在他的 dt.center[] 数组中添加起点的 id
*/
center?: string[]
/**
* A连线(又称物体流动线)的输入, 关系的终点需要在 dt.in[] 数组中添加起点的 id
*/
in?: string[]
/**
* A连线(又称物体流动线)的输出, 关系的起点需要在 dt.out[] 数组中添加目标点的 id
*/
out?: string[]
/**
* 是否可以被选中, 默认 true
*/
selectable?: boolean
/**
* 是否受保护, 不可在图形编辑器中拖拽, 默认 false
*/
protected?: boolean
/**
* 其他自定义数据, 可以存储任何数据
*/
[key: string]: any
},
}
场景和视窗
场景 SceneHelp
/**
* 场景对象
* 通常是某个楼层的所有物品和摆放, 每个场景可能会有多个不同的 Viewport 对他进行观察
* 这是一个成熟的类, 不用对他改造
*/
export default class SceneHelp {
scene: THREE.Scene
axesHelper: THREE.GridHelper
gridHelper: THREE.GridHelper
/**
* 整个仓库的地图模型
*/
worldModel: WorldModel
/**
* 实体管理器, 所有控制实体都在这里管理
*/
entityManager: EntityManager
constructor(floor: string)
}
状态管理器 DataStateManager
/**
* 地图数据状态的管理器, 他能够对数据进行增删改查,并且能够进行撤销、重做等操作.
* 所有的修改都应该从这里发起, 多数修改都是从各个物流单元的 interaction 发起
*/
export default class StateManager {
/**
* 唯一场景标识符, 用于做临时存储的 key
*/
id: string
/**
* 视口对象, 用于获取、同步当前场景的状态
*/
viewport: Viewport
/**
* 是否发生了变化,通知外部是否需要保存数据
*/
isChanged = ref(false)
/**
* 是否正在加载数据,通知外部是否需要等待加载完成
*/
isLoading = ref(false)
/**
* 当前场景数据
*/
vdata: VData = { items: [], isChanged: false }
constructor(id: string, viewport: Viewport)
/**
* 开始用户操作(创建数据快照)
*/
beginUpdate()
/**
* 结束用户操作(计算差异并保存), 内部会调用 syncDataState 方法, 换起实体管理器 EntityManager
*/
commitUpdate()
/**
* 将当前数据 与 EntityManager 进行同步, 对比出不同的部分,分别进行更新
* - 调用 viewport.entityManager.beginBatch() 开始更新
* - 调用 viewport.entityManager.createEntity(vdataItem) 添加场景中新的实体
* - 调用 viewport.entityManager.updateEntity(vdataItem) 新场景中已存在的实体
* - 调用 viewport.entityManager.deleteEntity(id) 删除场景中的实体
* - 调用 viewport.entityManager.commitBatch() 结束更新场景
*/
syncDataState()
/**
* 从外部加载数据
*/
async load(items: VDataItem[])
/**
* 保存数据到外部
*/
async save(): Promise<Object>
/**
* 撤销
*/
undo()
/**
* 重做
*/
redo()
/**
* 保存到本地存储 浏览器indexDb(防止数据丢失)
*/
async saveToLocalstore()
/**
* 从本地存储还原数据
*/
async loadFromLocalstore()
/**
* 删除本地存储
*/
async removeLocalstore()
}
视窗 Viewport
/**
* 视窗对象, 这是一个成熟的类, 不用对他改造
*/
export default class Viewport {
sceneHelp: SceneHelp
viewerDom: HTMLElement
camera: THREE.OrthographicCamera
renderer: THREE.WebGLRenderer
statsControls: Stats
controls: OrbitControls
raycaster: THREE.Raycaster
dragControl: EsDragControls
animationFrameId: any = null
constructor(sceneHelp: SceneHelp, viewerDom: HTMLElement)
/**
* 初始化 THREE 渲染器
*/
initThree(sceneHelp: SceneHelp, viewerDom: HTMLElement, floor: string)
/**
* 动画循环
*/
animate()
/**
* 销毁视窗
*/
destroy()
/**
* 获取坐标下所有对象
*/
getIntersects(point: THREE.Vector2): THREE.Object3D[]
/**
* 获取鼠标所在的 x,y,z 坐标
* 鼠标坐标是相对于 canvas 元素 (renderer.domElement) 元素的
*/
getClosestIntersection(e: MouseEvent)
}
实体管理器 EntityManager
/**
* 缓存所有实体和他们的关系, 在各个组件的渲染器会调用这个实体管理器, 进行检索 / 关系 / 获取差异等计算
*/
export class EntityManager {
/**
* 视窗对象, 所有状态管理器, ThreeJs场景,控制器,摄像机, 实体管理器都在这里
*/
viewport: Viewport
// 所有数据点的实体
entities = new Map<string, ItemJson>();
// 所有关联关系
relationIndex = new Map<string, {
center: Set<string>;
in: Set<string>;
out: Set<string>;
}>()
// 两两关联关系与THREEJS对象之间的关联
lines = new Map<[string, string], THREE.Object3D[]>();
constructor(viewport: Viewport)
// 批量更新开始
beginUpdate()
// 创建一个实体, 这个点的 center[] / in[] / out[] 关联的点, 都要对应进行关联
createEntity(entity: ItemJson, option?: EntityCudOption)
// 更新实体, 他可能更新位置, 也可能更新颜色, 也可能修改 dt.center[] / dt.in[] / dt.out[] 修正与其他点之间的关联
updateEntity(entity: ItemJson, option?: EntityCudOption)
// 删除实体, 与这个点有关的所有线都要删除, 与这个点有关系的点,都要对应的 center[] / in[] / out[] 都要断绝关系
deleteEntity(id: string, option?: EntityCudOption)
// createEntity / updateEntity / deleteEntity 调整完毕之后, 调用这个方法进行收尾
// 这个方法最重要的是进行连线逻辑的处理
// - 如果进行了添加, 那么这个点的 center[] / in[] / out[] 关联的点, 都要对应进行关联
// - 如果进行了删除, 与这个点有关的所有线都要删除, 与这个点有关系的点,都要对应的 center[] / in[] / out[] 都要断绝关系
// - 如果进行了更新, 如果改了颜色/位置, 则需要在UI上进行对应修改,如果改了关系,需要与关联的节点批量调整
// 将影响到的所有数据, 都变成一个修改集合, 统一调用对应单元类型渲染器(BaseRenderer)的 createPoint / deletePoint / updatePoint / createLine / updateLine / deleteLine 方法
// 具体方法就是 viewport.getItemTypeRenderer(itemTypeName)
commitUpdate()
// 获取实体
getEntity(id: string): ItemJson | undefined
// 获取相关实体
getRelatedEntities(id: string, relationType: 'center' | 'in' | 'out'): ItemJson[]
}
物流单元封装
物流单元组件封装, 每个组件类别都会有
- 渲染器 renderer
- 交互控制器 interaction
- 实体定义 entity
- 属性元数据 meta
比如测量组件 Measure, 他属于最基础的线类型物流单元.
渲染器 renderer
/**
* 基本渲染器基类
* 定义了点 / 线 该如何渲染到 ThreeJs 场景中, 这里可能会调用 InstancePool 进行渲染
* 每个物流单元类型, 全局只有一个实例
*/
public export default BaseRenderer {
// 开始更新, 可能暂停动画循环对本渲染器的动画等
beginUpdate(viewport: Viewport);
// 创建一个点, 每种物流单元类型不一样, 可能 measure 是创建一个红色方块, 可能 moveline 创建一个菱形, 可能 conveyor 创建一个齿轮
abstract createPoint(item: ItemJson, option?: RendererCudOption)
// 删除一个点
abstract deletePoint(id, option?: RendererCudOption);
// 更新一个点
abstract updatePoint(item: ItemJson, option?: RendererCudOption);
// 创建一根线, 每种物流单元类型不同 这个方法都不同
// 可能 measure 对于 in 和 out 忽略, center 就是创造一根 Line2,
// 可能 conveyor 对于 in 和 out 是创造一个 Mesh 并带动画 带纹理背景 带几何形状, center 就是画一个红色的细线
abstract createLine(start: ItemJson, end: ItemJson, type: 'in' | 'out' | 'center', option?: RendererCudOption)
// 更新一根线
abstract updateLine(start: ItemJson, end: ItemJson, type: 'in' | 'out' | 'center', option?: RendererCudOption)
// 删除一根线
abstract deleteLine(start: ItemJson, end: ItemJson, option?: RendererCudOption)
// 结束更新
abstract endUpdate(viewport: Viewport);
}
/**
* 辅助测量工具渲染器
*/
public export default class MeasureRenderer extends BaseRenderer {
// 开始更新, 可能暂停动画循环对本渲染器的动画等
beginUpdate(viewport: Viewport);
// 创建一个点
createPoint(item: ItemJson, option?: RendererCudOption)
// 删除一个点
deletePoint(id, option?: RendererCudOption);
// 更新一个点
updatePoint(item: ItemJson, option?: RendererCudOption)
// 创建一根线
createLine(start: ItemJson, end: ItemJson, type: 'in' | 'out' | 'center', option?: RendererCudOption)
// 更新一根线
updateLine(start: ItemJson, end: ItemJson, type: 'in' | 'out' | 'center', option?: RendererCudOption)
// 删除一根线
deleteLine(start: ItemJson, end: ItemJson, option?: RendererCudOption)
// 结束更新
endUpdate(viewport: Viewport);
}
实例池 InstancePool
{
不知道怎么做
}
交互控制器 interaction
/**
* 交互控制器基类
* 定义了在建模编辑器里面物流单元, 如何响应鼠标, 键盘的操作.
* 每个物流单元类型, 全局只有一个实例
*/
public default class BaseInteraction {
// 初始化
init(viewport: Viewport)
// 测量工具开始, 监听 Three.Renderer.domElement 的鼠标事件, 有可能用户会指定以某个点为起点, 也有可能第一个点由用户点击而来
start(startPoint?: THREE.Object3D)
// 用户鼠标移动时, 判断是否存在起点, 有起点就要实时构建临时线, 临时线的中间创建一个 Label 显示线的长度. 每次鼠标移动都要进行重新调整
onMousemove(e: MouseEvent)
// 用户如果点左键, 就调用 MeasureRenderer.createPoint 创建点, 如果点击右键就调用 Stop 退出工具
onMouseup(e: MouseEvent)
// 退出这个工具的点击, 停止对 Three.Renderer.domElement 的监听
stop()
// 用户在设计器拽动某个点时触发. 这时调整的都是 "虚点" 和 "虚线"
dragPointStart(point: THREE.Object3D)
// 用户在设计器拖拽完毕后触发, 他会触发 MeasureRenderer.updatePoint 事件
dragPointComplete()
// 在用户开始测量工具 start / 或拖拽 dragPointStart 过程中,会创建临时点和临时线, 辅助用户
// 临时点在一次交互中只会有一个
createOrUpdateTempPoint(e: MouseEvent)
// 临时线在一次交互中, 可能会有多个, 取决于调整的点 有多少关联点, 都要画出虚线和label
createOrUpdateTempLine(label: string, pointStart: THREE.Object3D, pointEnd: THREE.Object3D)
// 清空所有临时点和线
clearTemps()
}
public default class MeasureInteraction extends BaseInteraction {
// 用户在设计器拽动某个点时触发. 这时调整的都是 "虚点" 和 "虚线"
dragPointStart(point: THREE.Object3D)
// 用户在设计器拖拽完毕后触发, 他会触发 MeasureRenderer.updatePoint 事件
dragPointComplete()
}
属性元数据 meta
/**
* 他定义了数据如何呈现在属性面板, 编辑器如果修改, 配合 Entity 该如何进行
*/
export default {
// "点"属性面板
point: {
// 基础面板
basic: [
{ field: 'uuid', editor: 'UUID', label: 'uuid', readonly: true },
{ field: 'name', editor: 'TextInput', label: '名称' },
{ field: 'dt.label', editor: 'TextInput', label: '标签' },
{ editor: 'TransformEditor' },
{ field: 'dt.color', editor: 'Color', label: '颜色' },
{ editor: '-' },
{ field: 'tf', editor: 'InOutCenterEditor' },
{ field: 'dt.selectable', editor: 'Switch', label: '可选中' },
{ field: 'dt.protected', editor: 'Switch', label: '受保护' },
{ field: 'visible', editor: 'Switch', label: '可见' }
]
},
// "线"属性面板
line: {
...xxx
}
}
实体定义 entity
/**
* 基本"点"属性操作代理实体
* 这个对象不易大量存在, 只有在绑定控制面板, 高级属性对话框, 或操作 Modeltree, 或脚本控制的时候才被实例化
* 他提供操作的抽象代理, 最终可能调用 DataStateManager 进行数据的保存
*/
export default class BaseItemEntity {
setItem(itemJson: ItemJson)
setObject(point: THREE.Object3D)
}
/**
* 基本"线"属性操作代理实体
* 这个对象不易大量存在, 只有在绑定控制面板, 高级属性对话框, 或操作 Modeltree, 或脚本控制的时候提供Proxy操作代理
*/
export default class BaseLineEntity {
setItem(start: ItemJson, end: ItemJson)
setObjects(line: THREE.Object3D[])
}
/**
* "线"属性 操作代理实体
*/
export class MeasurePoint : BaseItemEntity {
... 各种 get / set / method
}
/**
* "线"属性 操作代理实体
*/
export class MeasureLine: BaseLineEntity {
... 各种 get / set / method
}
所有的模型操作都遵循如流程
DataStateManager -> EntityManager -> xxx.renderer -> InstancePool
辅助类的操作则是通过交互控制器
interaction 完成临时辅助对象的创建, 辅助线 / 临时单元 是不会被持久化的
点之间的关系不会非常复杂, 通常是比较稀疏的, 可能一个点最多有6个连线, 绝大部分点 只有1~2个关系连线.
在大规模制图, 比如单场景中存在 10000 个以上的点, 是否存在性能问题?
这种封装是否合理, 有什么优化建议?