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

物流模型总体介绍

基本定义

物流单元大纲

  • 点 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 个以上的点, 是否存在性能问题?

这种封装是否合理, 有什么优化建议?