# 物流模型总体介绍 ## 基本定义 ### 物流单元大纲 - 点 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 ```ts /** * 物流世界模型 */ 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 } ``` 物流控制中心系统,基于 ThreeJS 和 Vue3 开发. 他的主要作用就是通过Web浏览器, 建立一个自动化立体仓库的物流模型 ### 单元点 ItemJson ```ts /** * 物体单元(点) */ 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 ```ts /** * 场景对象 * 通常是某个楼层的所有物品和摆放, 每个场景可能会有多个不同的 Viewport 对他进行观察 * 这是一个成熟的类, 不用对他改造 */ export default class SceneHelp { scene: THREE.Scene axesHelper: THREE.GridHelper gridHelper: THREE.GridHelper /** * 整个仓库的地图模型 */ worldModel: WorldModel /** * 实体管理器, 所有控制实体都在这里管理 */ entityManager: EntityManager constructor(floor: string) } ``` ### 状态管理器 DataStateManager ```ts /** * 地图数据状态的管理器, 他能够对数据进行增删改查,并且能够进行撤销、重做等操作. * 所有的修改都应该从这里发起, 多数修改都是从各个物流单元的 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 /** * 撤销 */ undo() /** * 重做 */ redo() /** * 保存到本地存储 浏览器indexDb(防止数据丢失) */ async saveToLocalstore() /** * 从本地存储还原数据 */ async loadFromLocalstore() /** * 删除本地存储 */ async removeLocalstore() } ``` ### 视窗 Viewport ```ts /** * 视窗对象, 这是一个成熟的类, 不用对他改造 */ 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 ```ts /** * 缓存所有实体和他们的关系, 在各个组件的渲染器会调用这个实体管理器, 进行检索 / 关系 / 获取差异等计算 */ export class EntityManager { /** * 视窗对象, 所有状态管理器, ThreeJs场景,控制器,摄像机, 实体管理器都在这里 */ viewport: Viewport // 所有数据点的实体 entities = new Map(); // 所有关联关系 relationIndex = new Map; in: Set; out: Set; }>() // 两两关联关系与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 ```ts /** * 基本渲染器基类 * 定义了点 / 线 该如何渲染到 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 ```ts { 不知道怎么做 } ``` ### 交互控制器 interaction ```ts /** * 交互控制器基类 * 定义了在建模编辑器里面物流单元, 如何响应鼠标, 键盘的操作. * 每个物流单元类型, 全局只有一个实例 */ 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 ```ts /** * 他定义了数据如何呈现在属性面板, 编辑器如果修改, 配合 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 ```ts /** * 基本"点"属性操作代理实体 * 这个对象不易大量存在, 只有在绑定控制面板, 高级属性对话框, 或操作 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 个以上的点, 是否存在性能问题? 这种封装是否合理, 有什么优化建议?