Browse Source

大重构

master
修宁 7 months ago
parent
commit
b4bbcff248
  1. 81
      README.md
  2. 658
      doc/物流模型总体介绍.md
  3. BIN
      src/assets/images/conveyor/shapes/logo.png
  4. 2
      src/components/Model3DView.vue
  5. 0
      src/core/Constract.ts
  6. 66
      src/core/ModelUtils.ts
  7. 35
      src/core/base/BaseInteraction.ts
  8. 26
      src/core/base/BaseItemEntity.ts
  9. 85
      src/core/base/BaseRenderer.ts
  10. 55
      src/core/base/IMeta.ts
  11. 2
      src/core/controls/DragControls.js
  12. 13
      src/core/controls/EsDragControls.ts
  13. 2
      src/core/controls/IControls.ts
  14. 6
      src/core/controls/MouseMoveInspect.ts
  15. 8
      src/core/controls/SelectInspect.ts
  16. 133
      src/core/engine/SceneHelp.ts
  17. 153
      src/core/engine/Viewport.ts
  18. 157
      src/core/manager/EntityManager.ts
  19. 151
      src/core/manager/InstancePool.ts
  20. 50
      src/core/manager/InteractionManager.ts
  21. 82
      src/core/manager/ModuleManager.ts
  22. 72
      src/core/manager/StateManager.ts
  23. 180
      src/core/manager/WorldModel.ts
  24. 8
      src/designer/ModelView.vue
  25. 73
      src/designer/model2DEditor/Model2DEditor.vue
  26. 91
      src/designer/model2DEditor/Model2DEditorJs.js
  27. 213
      src/editor/Model2DEditor.vue
  28. 8
      src/editor/Model3DViewer.vue
  29. 0
      src/editor/ModelMain.less
  30. 22
      src/editor/ModelMain.vue
  31. 34
      src/editor/ModelMainInit.ts
  32. 2
      src/editor/menus/EditMenu.ts
  33. 9
      src/editor/menus/FileMenu.ts
  34. 2
      src/editor/menus/Model3DView.ts
  35. 0
      src/editor/menus/Tools.ts
  36. 0
      src/editor/propEditors/ColorItem.vue
  37. 6
      src/editor/propEditors/IMetaProp.ts
  38. 0
      src/editor/propEditors/NumberInput.vue
  39. 0
      src/editor/propEditors/SwitchItem.vue
  40. 0
      src/editor/propEditors/TextInput.vue
  41. 0
      src/editor/propEditors/Transform.vue
  42. 0
      src/editor/propEditors/UUIDItem.vue
  43. 6
      src/editor/widgets/IWidgets.ts
  44. 0
      src/editor/widgets/alarm/AlarmMeta.ts
  45. 0
      src/editor/widgets/alarm/AlarmView.vue
  46. 0
      src/editor/widgets/logger/LoggerMeta.ts
  47. 0
      src/editor/widgets/logger/LoggerView.vue
  48. 0
      src/editor/widgets/modeltree/ModeltreeMeta.ts
  49. 4
      src/editor/widgets/modeltree/ModeltreeView.vue
  50. 23
      src/editor/widgets/modeltree/ModeltreeViewJs.js
  51. 0
      src/editor/widgets/monitor/MonitorMeta.ts
  52. 0
      src/editor/widgets/monitor/MonitorView.vue
  53. 0
      src/editor/widgets/property/PropertyMeta.ts
  54. 12
      src/editor/widgets/property/PropertyView.vue
  55. 0
      src/editor/widgets/script/ScriptMeta.ts
  56. 2
      src/editor/widgets/script/ScriptView.vue
  57. 0
      src/editor/widgets/task/TaskMeta.ts
  58. 0
      src/editor/widgets/task/TaskView.vue
  59. 0
      src/editor/widgets/toolbox/ToolboxMeta.ts
  60. 0
      src/editor/widgets/toolbox/ToolboxView.vue
  61. 61
      src/example/example1.js
  62. 186
      src/model/WorldModel.ts
  63. 155
      src/model/WorldModelType.ts
  64. 3
      src/model/itemType/ItemTypeLine.ts
  65. 2
      src/model/itemType/ToolboxLine.ts
  66. 5
      src/modules/measure/MeasureEntity.ts
  67. 18
      src/modules/measure/MeasureInteraction.ts
  68. 23
      src/modules/measure/MeasureMeta.ts
  69. 40
      src/modules/measure/MeasureRenderer.ts
  70. 13
      src/modules/measure/index.ts
  71. 2
      src/router/index.ts
  72. 9
      src/runtime/EventBus.js
  73. 17
      src/runtime/EventBus.ts
  74. 4
      src/runtime/System.ts
  75. 50
      src/types/Types.d.ts
  76. 2
      src/types/global.d.ts
  77. 181
      src/types/model.d.ts
  78. 33
      src/utils/webutils.ts

81
README.md

@ -1,19 +1,70 @@
# yvan-rcs-web
## Project Setup
```sh
yarn
```
### Compile and Hot-Reload for Development
```sh
yarn dev
## 文件结构构成
```
### Type-Check, Compile and Minify for Production
```sh
yarn build
src/
├── assets/ # 静态资源(纹理、图标等)
├── components/ # 一些公共组件
├── core/ # 核心类库(不依赖 Vue,便于复用)
│ ├── example/ # 各种实体基础类
│ │ └── Example1.js
│ ├── base/ # 各种实体基础类
│ │ ├── BaseRenderer.ts
│ │ ├── BaseInteraction.ts
│ │ ├── BaseMeta.ts
│ │ ├── BaseItemEntity.ts
│ │ └── BaseLineEntity.ts
│ ├── manager/ # 管理器类
│ │ ├── ModuleManager.ts
│ │ ├── StateManager.ts
│ │ ├── WorldModel.ts
│ │ ├── EntityManager.ts
│ │ └── InstancePool.ts
│ ├── utils/ # 管理器类
│ │ ├── StateManager.ts
│ │ └── WorldModel.ts
│ └── engine/ # Three.js 封装类
│ ├── SceneHelp.ts
│ └── Viewport.ts
├── editor/ # 编辑器
│ ├── menus/ # 各种实体基础类
│ │ ├── FileMenu.ts
│ │ ├── EditMenu.ts
│ │ ├── Model3DView.ts
│ │ └── Tools.ts
│ ├── widgets/ # 管理器类
│ │ └── ...
│ ├── propEditors/ # 属性面板编辑器
│ │ └── ...
│ ├── controls/ # 各种实体基础类
│ │ ├── SelectionControls.ts
│ │ ├── EsDragControls.ts
│ │ └── MouseMoveControls.ts
│ ├── Model3DViewer.vue
│ ├── Model2DEditor.vue
│ └── EditorMain.vue # Three.js 封装类
├── modules/ # 模块化插件目录(按物流单元类型组织)
│ ├── measure/ # 测量单元模块
│ │ ├── MeasureRenderer.ts
│ │ ├── MeasureInteraction.ts
│ │ ├── MeasureMeta.ts
│ │ ├── MeasureEntity.ts
│ │ └── index.ts
│ ├── conveyor/ # 输送线模块
│ │ ├── ConveyorRenderer.ts
│ │ ├── ConveyorInteraction.ts
│ │ ├── ConveyorMeta.ts
│ │ ├── ConveyorEntity.ts
│ │ └── index.ts
│ └── ... # 其他物流单元模块
├── plugins/ # 插件系统支持
│ └── registerItemType.ts # 注册物流单元类型的插件机制
├── types/ # 类型定义(全局共享的类型)
│ ├── model.d.ts
│ └── index.d.ts
├── utils/ # 工具函数(非 Three 相关)
│ └── index.ts
└── views/ # 页面视图(Vue 页面)
├── Editor.vue # 主编辑器页面
└── Viewer.vue # 查看器页面
```

658
doc/物流模型总体介绍.md

@ -0,0 +1,658 @@
# 物流模型总体介绍
## 基本定义
### 物流单元大纲
- 点 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<ItemJson[]>
}
```
物流控制中心系统,基于 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<Object>
/**
* 撤销
*/
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<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
```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 个以上的点, 是否存在性能问题?
这种封装是否合理, 有什么优化建议?

BIN
src/assets/images/conveyor/shapes/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

2
src/designer/model3DView/Model3DView.vue → src/components/Model3DView.vue

@ -88,7 +88,7 @@ import { renderIcon } from '@/utils/webutils.js'
import textureUrl from '@/assets/images/conveyor/shapes/RibSideSkirtThumbnail.jpg'
import moveUrl from '@/assets/images/conveyor/shapes/move.svg'
import arrowRightUrl from '@/assets/images/conveyor/shapes/arrow-right.svg'
// import rackUrl from '@/assets/images/conveyor/shapes/Rack.png'
import rackUrl from '@/assets/images/conveyor/shapes/Rack.png'
// DOM refs
const canvasContainer = ref(null)

0
src/designer/Constract.ts → src/core/Constract.ts

66
src/model/ModelUtils.ts → src/core/ModelUtils.ts

@ -1,44 +1,44 @@
import * as THREE from 'three'
import type { ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine.ts'
import type { ItemJson } from '@/model/WorldModelType.ts'
import { getAllItemTypes, getItemTypeByName } from '@/model/itemType/ItemTypeDefine.ts'
import type Viewport from '@/designer/Viewport.ts'
import type Viewport from '@/core/engine/Viewport'
import { computeBoundsTree, disposeBoundsTree } from 'three-mesh-bvh'
import { Vector2 } from 'three/src/math/Vector2'
import type Toolbox from '@/model/itemType/Toolbox.ts'
export function deletePointByKeyboard() {
const viewport: Viewport = window['viewport']
if (!viewport) {
system.msg('没有找到当前视图')
return
}
// 按下 Delete 键,删除当前选中的点
if (!viewport.state.selectedObject) {
system.msg('没有选中任何点')
return
}
const selectedObject = viewport.state.selectedObject
if (!(selectedObject instanceof THREE.Object3D)) {
system.msg('选中的对象不是有效的点')
return
}
if (!selectedObject.userData?.type) {
system.msg('选中的对象没有类型信息')
return
}
const toolbox: Toolbox = viewport.toolbox[selectedObject.userData.type]
if (!toolbox) {
system.msg('没有找到对应的工具箱')
return
}
viewport.state.cursorMode = 'normal'
toolbox.deletePoint(selectedObject)
system.msg('Delete not impleted yet')
// const viewport: Viewport = window['viewport']
// if (!viewport) {
// system.msg('没有找到当前视图')
// return
// }
//
// // 按下 Delete 键,删除当前选中的点
// if (!viewport.state.selectedObject) {
// system.msg('没有选中任何点')
// return
// }
//
// const selectedObject = viewport.state.selectedObject
// if (!(selectedObject instanceof THREE.Object3D)) {
// system.msg('选中的对象不是有效的点')
// return
// }
//
// if (!selectedObject.userData?.type) {
// system.msg('选中的对象没有类型信息')
// return
// }
//
// const toolbox: Toolbox = viewport.toolbox[selectedObject.userData.type]
// if (!toolbox) {
// system.msg('没有找到对应的工具箱')
// return
// }
//
// viewport.state.cursorMode = 'normal'
// toolbox.deletePoint(selectedObject)
}
export function escByKeyboard() {

35
src/core/base/BaseInteraction.ts

@ -0,0 +1,35 @@
import * as THREE from 'three'
import type Viewport from '@/core/engine/Viewport'
/**
*
*
*/
export default abstract class BaseInteraction {
protected viewport!: Viewport
/**
*
* @param viewport
* @param startPoint
*/
abstract start(viewport: Viewport, startPoint?: THREE.Object3D): void
/**
*
*/
abstract stop(): void
/**
*
* @param viewport
* @param point
*/
abstract dragPointStart(viewport: Viewport, point: THREE.Object3D): void
/**
*
* @param viewport
*/
abstract dragPointComplete(viewport: Viewport): void
}

26
src/core/base/BaseItemEntity.ts

@ -0,0 +1,26 @@
import * as THREE from 'three'
/**
* BaseEntity class
* Provides a base for managing logistics unit entities.
*/
export default abstract class BaseEntity {
protected itemJson!: ItemJson
protected objects!: THREE.Object3D[]
/**
* Sets the `ItemJson` data for the entity.
* @param itemJson - The `ItemJson` data to set.
*/
setItem(itemJson: ItemJson): void {
this.itemJson = itemJson
}
/**
* Sets the `THREE.Object3D` object for the entity.
* @param object3D - The `THREE.Object3D` object to set.
*/
setObjects(objects: THREE.Object3D[]): void {
this.objects = objects
}
}

85
src/core/base/BaseRenderer.ts

@ -0,0 +1,85 @@
import type Viewport from '@/core/engine/Viewport'
/**
*
* / 线 Three.js
*/
export default abstract class BaseRenderer {
/**
*
* @param viewport
*/
beginUpdate(viewport: Viewport): void {
// Optional: Pause animations or prepare for batch updates
}
/**
*
* @param item
* @param option
*/
abstract createPoint(item: ItemJson, option?: RendererCudOption): void
/**
*
* @param id
* @param option
*/
abstract deletePoint(id: string, option?: RendererCudOption): void
/**
*
* @param item
* @param option
*/
abstract updatePoint(item: ItemJson, option?: RendererCudOption): void
/**
* 线
* @param start
* @param end
* @param type 线
* @param option
*/
abstract createLine(
start: ItemJson,
end: ItemJson,
type: 'in' | 'out' | 'center',
option?: RendererCudOption
): void
/**
* 线
* @param start
* @param end
* @param type 线
* @param option
*/
abstract updateLine(
start: ItemJson,
end: ItemJson,
type: 'in' | 'out' | 'center',
option?: RendererCudOption
): void
/**
* 线
* @param start
* @param end
* @param option
*/
abstract deleteLine(
start: ItemJson,
end: ItemJson,
option?: RendererCudOption
): void
/**
*
* @param viewport
*/
endUpdate(viewport: Viewport): void {
// Optional: Resume animations or finalize batch updates
}
}

55
src/core/base/IMeta.ts

@ -0,0 +1,55 @@
import type { ItemTypeMeta } from '@/model/itemType/ItemTypeDefine.ts'
/**
* "点"
*/
export const BASIC_META_OF_POINT: ItemTypeMeta = [
{ field: 'uuid', editor: 'UUID', label: 'uuid', readonly: true },
{ field: 'name', editor: 'TextInput', label: '名称' },
{ field: 'userData.label', editor: 'TextInput', label: '标签' },
{ editor: 'Transform' },
{ field: 'color', editor: 'Color', label: '颜色' },
{ editor: '-' },
{ editor: 'IN_OUT_CENTER' }
]
/**
* "物流运输单元",
*/
export const BASIC_META_OF_POINT2: ItemTypeMeta = [
{ field: 'userData.selectable', editor: 'Switch', label: '可选中' },
{ field: 'userData.protected', editor: 'Switch', label: '受保护' },
{ field: 'visible', editor: 'Switch', label: '可见' }
]
/**
* "线"
*/
export const BASIC_META_OF_LINE: ItemTypeMeta = []
/**
* "线",
*/
export const BASIC_META_OF_LINE2: ItemTypeMeta = []
/**
* , category, tabName, MetaItem
*/
export interface IMeta {
[key: string]: {
[tabName: string]: MetaItem[]
}
}
/**
* PropertyPanelConfig interface
* Defines the structure of property panel configurations.
*/
export interface MetaItem {
field?: string;
editor: string;
label?: string;
readonly?: boolean;
[key: string]: any;
}

2
src/designer/model2DEditor/DragControls.js → src/core/controls/DragControls.js

@ -6,7 +6,7 @@ import {
Vector2,
Vector3
} from 'three'
import { calcPositionUseSnap } from '@/model/ModelUtils.js'
import { calcPositionUseSnap } from '@/core/ModelUtils.js'
const _plane = new Plane()
const _raycaster = new Raycaster()

13
src/designer/model2DEditor/EsDragControls.ts → src/core/controls/EsDragControls.ts

@ -1,9 +1,8 @@
import { DragControls } from './DragControls.js'
import * as THREE from 'three'
import type Viewport from '@/designer/Viewport.ts'
import Constract from '@/designer/Constract.ts'
import { getItemTypeByName } from '@/model/itemType/ItemTypeDefine.ts'
import type { ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine.ts'
import { DragControls } from './DragControls.js'
import type Viewport from '@/core/engine/Viewport.ts'
import { getItemTypeByName } from '@/model/itemType/ItemTypeDefine'
import type { ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine'
import { markRaw } from 'vue'
import EventBus from '@/runtime/EventBus'
@ -96,7 +95,7 @@ export default class EsDragControls {
// 拖拽中
drag(e) {
EventBus.$emit('objectChanged', {
EventBus.dispatch('objectChanged', {
viewport: this,
object: e.object
})
@ -121,7 +120,7 @@ export default class EsDragControls {
}
if (e.object.userData.selectable) {
this.viewport.state.selectedObject = markRaw(e.object)
EventBus.$emit('objectChanged', {
EventBus.dispatch('objectChanged', {
viewport: this,
object: e.object
})

2
src/designer/model2DEditor/tools/ITool.ts → src/core/controls/IControls.ts

@ -1,4 +1,4 @@
export interface ITool {
export default interface IControls {
init(viewport: any): void
destory(): void

6
src/designer/model2DEditor/tools/MouseMoveInspect.ts → src/core/controls/MouseMoveInspect.ts

@ -1,5 +1,5 @@
import type Viewport from '@/designer/Viewport.ts'
import type { ITool } from '@/designer/model2DEditor/tools/ITool.ts'
import type Viewport from '@/core/engine/Viewport'
import type IControls from './IControls'
import * as THREE from 'three'
let pmFn, otFn, lvFn
@ -7,7 +7,7 @@ let pmFn, otFn, lvFn
/**
* designer.mousePos
*/
export default class MouseMoveInspect implements ITool {
export default class MouseMoveInspect implements IControls {
viewport: Viewport
canvas: HTMLCanvasElement

8
src/designer/model2DEditor/tools/SelectInspect.ts → src/core/controls/SelectInspect.ts

@ -1,7 +1,7 @@
import * as THREE from 'three'
import type { ITool } from './ITool.ts'
import type IControls from './IControls'
import { watch } from 'vue'
import type Viewport from '@/designer/Viewport.ts'
import type Viewport from '@/core/engine/Viewport'
import { Line2 } from 'three/examples/jsm/lines/Line2.js'
import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js'
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js'
@ -13,7 +13,7 @@ let pdFn, pmFn, puFn
/**
*
*/
export default class SelectInspect implements ITool {
export default class SelectInspect implements IControls {
viewport: Viewport
/**
* 线
@ -64,7 +64,7 @@ export default class SelectInspect implements ITool {
this.canvas.addEventListener('pointerup', puFn)
this.viewport.watchList.push(watch(() => this.viewport.state.selectedObject, this.updateSelectionBox.bind(this)))
EventBus.$on('objectChanged', (data) => {
EventBus.on('objectChanged', (data) => {
this.updateSelectionBox(this.viewport.state.selectedObject)
})
}

133
src/core/engine/SceneHelp.ts

@ -0,0 +1,133 @@
import * as THREE from 'three'
import type WorldModel from '@/core/manager/WorldModel'
/**
*
* Three.js
*/
export default class SceneHelp {
scene: THREE.Scene
axesHelper: THREE.GridHelper
gridHelper: THREE.GridHelper
worldModel: WorldModel
catalogCode: string
/**
*
* @param worldModel
* @param catalogCode ID
*/
constructor(worldModel: WorldModel, catalogCode: string) {
this.worldModel = worldModel
this.catalogCode = catalogCode
// 初始化 Three.js 场景
this.scene = new THREE.Scene()
this.scene.background = new THREE.Color(0xeeeeee)
// 辅助线
const gridOption = this.worldModel.gridOption
const axesHelper = new THREE.GridHelper(gridOption.axesSize, gridOption.axesDivisions)
axesHelper.material.color.setHex(gridOption.axesColor)
axesHelper.material.linewidth = 2
axesHelper.material.opacity = gridOption.gridOpacity
axesHelper.material.transparent = true
if (!gridOption.axesEnabled) {
axesHelper.visible = false
}
// @ts-ignore
axesHelper.material.vertexColors = false
this.axesHelper = axesHelper
this.scene.add(this.axesHelper)
const gridHelper = new THREE.GridHelper(gridOption.gridSize, gridOption.gridDivisions)
gridHelper.material.color.setHex(gridOption.gridColor)
gridHelper.material.opacity = gridOption.gridOpacity
gridHelper.material.transparent = true
// @ts-ignore
gridHelper.material.vertexColors = false
if (!gridOption.gridEnabled) {
gridHelper.visible = false
}
this.gridHelper = gridHelper
this.scene.add(this.gridHelper)
// 光照
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8)
this.scene.add(ambientLight)
// const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5)
// directionalLight.position.set(5, 5, 5).multiplyScalar(3)
// directionalLight.castShadow = true
// scene.add(directionalLight)
//
// const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1)
// scene.add(hemisphereLight)
}
// /**
// * 加载指定楼层的实体并添加到场景
// * @param floorId 楼层 ID
// */
// async loadFloorEntities(floorId: string): Promise<void> {
// const items = await this.worldModel.loadFloor(floorId)
// items.forEach((item) => {
// this.entityManager.createEntity(item)
// })
// }
remove(...object: THREE.Object3D[]) {
this.scene.remove(...object)
}
add(...object: THREE.Object3D[]) {
this.scene.add(...object)
}
/**
* , WebGL
*/
destory() {
// 移除旧模型
if (!this.scene) {
return
}
this.scene.traverse((obj: any) => {
// 释放几何体
if (obj.geometry) {
obj.geometry.dispose()
}
// 释放材质
if (obj.material) {
if (Array.isArray(obj.material)) {
obj.material.forEach(m => m.dispose())
} else {
obj.material.dispose()
}
}
// 释放纹理
if (obj.texture) {
obj.texture.dispose()
}
// 释放渲染目标
if (obj.renderTarget) {
obj.renderTarget.dispose()
}
// 移除事件监听(如 OrbitControls)
if (obj.dispose) {
obj.dispose()
}
})
// 清空场景
this.scene.children = []
this.scene = null
}
}

153
src/designer/Viewport.ts → src/core/engine/Viewport.ts

@ -1,22 +1,24 @@
import _ from 'lodash'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import EsDragControls from './model2DEditor/EsDragControls'
import Stats from 'three/examples/jsm/libs/stats.module'
import type WorldModel from '@/model/WorldModel.ts'
import type WorldModel from '../manager/WorldModel'
import $ from 'jquery'
import { reactive, watch } from 'vue'
import type { ITool } from '@/designer/model2DEditor/tools/ITool.ts'
import type IControls from '../controls/IControls'
import { CSS3DRenderer } from 'three/examples/jsm/renderers/CSS3DRenderer'
import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer'
import { getAllItemTypes, type ItemTypeMeta } from '@/model/itemType/ItemTypeDefine.ts'
import type { ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine.ts'
import type Toolbox from '@/model/itemType/Toolbox.ts'
import { calcPositionUseSnap } from '@/model/ModelUtils.ts'
import SelectInspect from '@/designer/model2DEditor/tools/SelectInspect.ts'
import MouseMoveInspect from '@/designer/model2DEditor/tools/MouseMoveInspect.ts'
import type { CursorMode, VDataItem } from '@/types/Types'
import SceneHelp from './SceneHelp'
import SelectInspect from '../controls/SelectInspect'
import MouseMoveInspect from '../controls/MouseMoveInspect'
import EsDragControls from '../controls/EsDragControls'
import EntityManager from '../manager/EntityManager'
import InteractionManager from '@/core/manager/InteractionManager'
import { calcPositionUseSnap } from '@/core/ModelUtils'
import StateManager from '@/core/manager/StateManager.ts'
/**
*
@ -24,28 +26,28 @@ import type { CursorMode, VDataItem } from '@/types/Types'
*/
export default class Viewport {
viewerDom: HTMLElement
scene: THREE.Scene
camera: THREE.OrthographicCamera
renderer: THREE.WebGLRenderer
axesHelper: THREE.GridHelper
gridHelper: THREE.GridHelper
statsControls: Stats
controls: OrbitControls
worldModel: WorldModel
raycaster: THREE.Raycaster
dragControl: any // EsDragControls
animationFrameId: any = null
scene: SceneHelp
//搭配 state.cursorMode = xxx 之后, currentTool.start(第一个参数) 使用
toolStartObject: THREE.Object3D | null = null
currentTool: Toolbox | null = null
tools: ITool[] = [
tools: IControls[] = [
new MouseMoveInspect(),
new SelectInspect()
]
toolbox: Record<string, Toolbox> = {}
objectMap: Map<string, THREE.Object3D> = new Map()
// 状态管理器
stateManager: StateManager
// 实体管理器
entityManager = new EntityManager()
// 交互管理器
interactionManager = new InteractionManager()
beginSync() {
}
@ -62,6 +64,18 @@ export default class Viewport {
endSync() {
}
get worldModel(): WorldModel {
return this.scene.worldModel
}
get axesHelper(): THREE.GridHelper {
return this.scene.axesHelper
}
get gridHelper(): THREE.GridHelper {
return this.scene.gridHelper
}
/**
*
*/
@ -77,7 +91,6 @@ export default class Viewport {
//@ts-ignore
state: ViewportState = reactive({
currentFloor: '',
isReady: false,
cursorMode: 'normal',
selectedObject: null,
@ -91,23 +104,22 @@ export default class Viewport {
}
})
constructor(worldModel: WorldModel) {
this.worldModel = worldModel
constructor(sceneHelp: SceneHelp, viewerDom: HTMLElement) {
this.scene = sceneHelp
this.viewerDom = viewerDom
}
/**
* THREE
*/
initThree(viewerDom: HTMLElement, floor: string) {
console.log('viewport on floor', floor)
this.state.currentFloor = floor
this.viewerDom = viewerDom
const rect = viewerDom.getBoundingClientRect()
this.worldModel.registerViewport(this)
initThree(option: InitThreeOption) {
console.log('viewport on catelogCode: ' + this.scene.catalogCode)
const viewerDom = this.viewerDom
// 场景
const scene = this.worldModel.getSceneByFloor(this, this.state.currentFloor)
this.scene = scene
// 初始化各种管理器
this.entityManager.init(this)
this.interactionManager.init(this)
this.stateManager = new StateManager(option.stateManagerId, this)
// 渲染器
const renderer = new THREE.WebGLRenderer({
@ -163,47 +175,6 @@ export default class Viewport {
// 注册拖拽组件
this.dragControl = new EsDragControls(this)
// 辅助线
const gridOption = this.worldModel.gridOption
const axesHelper = new THREE.GridHelper(gridOption.axesSize, gridOption.axesDivisions)
axesHelper.material.color.setHex(gridOption.axesColor)
axesHelper.material.linewidth = 2
axesHelper.material.opacity = gridOption.gridOpacity
axesHelper.material.transparent = true
if (!gridOption.axesEnabled) {
axesHelper.visible = false
}
// @ts-ignore
axesHelper.material.vertexColors = false
this.axesHelper = axesHelper
this.scene.add(this.axesHelper)
const gridHelper = new THREE.GridHelper(gridOption.gridSize, gridOption.gridDivisions)
gridHelper.material.color.setHex(gridOption.gridColor)
gridHelper.material.opacity = gridOption.gridOpacity
gridHelper.material.transparent = true
// @ts-ignore
gridHelper.material.vertexColors = false
if (!gridOption.gridEnabled) {
gridHelper.visible = false
}
this.gridHelper = gridHelper
this.scene.add(this.gridHelper)
// 光照
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8)
scene.add(ambientLight)
// const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5)
// directionalLight.position.set(5, 5, 5).multiplyScalar(3)
// directionalLight.castShadow = true
// scene.add(directionalLight)
//
// const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1)
// scene.add(hemisphereLight)
// 性能监控
const statsControls = new Stats()
this.statsControls = statsControls
@ -223,34 +194,6 @@ export default class Viewport {
}
this.updateGridVisibility()
}))
this.watchList.push(watch(() => this.state.cursorMode, (newVal: CursorMode) => {
if (!this.state.isReady) {
return
}
if (this.currentTool) {
this.currentTool.stop()
this.currentTool = null
}
if (newVal === 'normal' || !newVal) {
this.dragControl.dragControls.enabled = true
return
}
const currentTool = this.toolbox[newVal]
if (currentTool) {
// 选择标尺工具
this.currentTool = currentTool
this.dragControl.dragControls.enabled = false
} else {
system.showErrorDialog(`当前鼠标模式 ${newVal} 不支持`)
}
if (this.currentTool) {
this.currentTool.start(this.toolStartObject)
this.toolStartObject = null
}
}))
// 监听窗口大小变化
if (this.resizeObserver) {
@ -332,19 +275,21 @@ export default class Viewport {
this.animationFrameId = requestAnimationFrame(this.animate.bind(this))
this.renderView()
if(window['lineMaterial']) {
this.offset -= 0.002
window['lineMaterial'].dashOffset = this.offset
}
}
/**
*
*/
renderView() {
this.statsControls?.update()
this.renderer?.render(this.scene, this.camera)
this.renderer?.render(this.scene.scene, this.camera)
this.css2DRenderer.render(this.scene, this.camera)
this.css3DRenderer.render(this.scene, this.camera)
this.css2DRenderer.render(this.scene.scene, this.camera)
this.css3DRenderer.render(this.scene.scene, this.camera)
}
/**
@ -377,7 +322,7 @@ export default class Viewport {
handleResize(entries: any) {
for (let entry of entries) {
// entry.contentRect包含了元素的尺寸信息
console.log('Element size changed:', entry.contentRect)
// console.log('Element size changed:', entry.contentRect)
const width = entry.contentRect.width
const height = entry.contentRect.height
@ -452,8 +397,6 @@ export default class Viewport {
this.resizeObserver = undefined
}
this.worldModel.unregisterViewport(this)
if (this.statsControls) {
this.statsControls.dom.remove()
}

157
src/core/manager/EntityManager.ts

@ -0,0 +1,157 @@
import * as THREE from 'three'
import { getRenderer } from './ModuleManager'
import type Viewport from '@/core/engine/Viewport.ts'
/**
* , , / /
*/
export default class EntityManager {
/**
* , , ThreeJs场景,,,
*/
viewport: Viewport
/**
*
*/
entities = new Map<string, ItemJson>()
/**
* THREEJS
*/
objects = new Map<string, THREE.Object3D[]>()
/**
*
*/
relationIndex = new Map<string, { center: Set<string>; in: Set<string>; out: Set<string> }>()
/**
* THREEJS
*/
lines = new Map<string, THREE.Object3D[]>()
private batchMode = false
init(viewport: Viewport) {
this.viewport = viewport
}
/**
*
*/
beginUpdate(): void {
this.batchMode = true
this.viewport.beginSync()
}
/**
* , center[] / in[] / out[] ,
*/
createEntity(entity: ItemJson, option?: EntityCudOption): void {
if (this.entities.has(entity.id!)) {
throw new Error(`Entity with ID "${entity.id}" already exists.`)
}
this.entities.set(entity.id!, entity)
this.updateRelations(entity)
const renderer = getRenderer(entity.t)
renderer.createPoint(entity, option)
}
/**
* , , , dt.center[] / dt.in[] / dt.out[]
*/
updateEntity(entity: ItemJson, option?: EntityCudOption): void {
if (!this.entities.has(entity.id!)) {
throw new Error(`Entity with ID "${entity.id}" does not exist.`)
}
this.entities.set(entity.id!, entity)
this.updateRelations(entity)
const renderer = getRenderer(entity.t)
renderer.updatePoint(entity, option)
}
/**
* , 线, , center[] / in[] / out[]
*/
deleteEntity(id: string, option?: EntityCudOption): void {
const entity = this.entities.get(id)
if (!entity) {
throw new Error(`Entity with ID "${id}" does not exist.`)
}
this.entities.delete(id)
this.removeRelations(id)
const renderer = getRenderer(entity.t)
renderer.deletePoint(id, option)
}
/**
* ,
* 线
* - , center[] / in[] / out[] ,
* - , 线, , center[] / in[] / out[]
* - , /, UI上进行对应修改
* , , (BaseRenderer) createPoint / deletePoint / updatePoint / createLine / updateLine / deleteLine
* viewport.getItemTypeRenderer(itemTypeName)
*/
commitUpdate(): void {
this.batchMode = false
this.viewport.endSync()
}
/**
*
*/
getEntity(id: string): ItemJson | undefined {
return this.entities.get(id)
}
/**
*
*/
getRelatedEntities(id: string, relationType: 'center' | 'in' | 'out'): ItemJson[] {
const relations = this.relationIndex.get(id)?.[relationType] || new Set()
return Array.from(relations).map((relatedId) => this.entities.get(relatedId)!)
}
private updateRelations(entity: ItemJson): void {
const { id, dt } = entity
if (!id || !dt) return
const relations = this.relationIndex.get(id) || { center: new Set(), in: new Set(), out: new Set() }
relations.center = new Set(dt.center || [])
relations.in = new Set(dt.in || [])
relations.out = new Set(dt.out || [])
this.relationIndex.set(id, relations)
// Update reverse relations
this.updateReverseRelations(id, dt.center, 'center')
this.updateReverseRelations(id, dt.in, 'out')
this.updateReverseRelations(id, dt.out, 'in')
}
private updateReverseRelations(id: string, relatedIds: string[] | undefined, relationType: 'center' | 'in' | 'out'): void {
if (!relatedIds) return
relatedIds.forEach((relatedId) => {
const relatedRelations = this.relationIndex.get(relatedId) || {
center: new Set(),
in: new Set(),
out: new Set()
}
relatedRelations[relationType].add(id)
this.relationIndex.set(relatedId, relatedRelations)
})
}
private removeRelations(id: string): void {
const relations = this.relationIndex.get(id)
if (!relations) return
// Remove reverse relations
relations.center.forEach((relatedId) => this.relationIndex.get(relatedId)?.center.delete(id))
relations.in.forEach((relatedId) => this.relationIndex.get(relatedId)?.out.delete(id))
relations.out.forEach((relatedId) => this.relationIndex.get(relatedId)?.in.delete(id))
this.relationIndex.delete(id)
}
}

151
src/core/manager/InstancePool.ts

@ -0,0 +1,151 @@
import * as THREE from 'three'
export class InstancePool {
private mesh: THREE.InstancedMesh
private maxCount: number
private nextIndex: number = 0
private freeIndices: number[] = []
private matrixArray: Float32Array
private matrixTexture: THREE.DataTexture | null = null
private needsUpdate: boolean = false
private visibleCount: number = 0
constructor(
geometry: THREE.BufferGeometry,
material: THREE.Material | THREE.Material[],
maxCount: number
) {
this.maxCount = maxCount
// 创建实例化网格
this.mesh = new THREE.InstancedMesh(geometry, material, maxCount)
this.mesh.frustumCulled = false // 禁用视锥剔除,由我们手动控制
// 初始化矩阵数组
this.matrixArray = new Float32Array(maxCount * 16)
// 初始将所有实例移到屏幕外
this.resetAllInstances()
}
// 获取一个可用实例
public acquireInstance(): number | null {
let index: number
if (this.freeIndices.length > 0) {
index = this.freeIndices.pop()!
} else if (this.nextIndex < this.maxCount) {
index = this.nextIndex++
} else {
console.warn('Instance pool exhausted')
return null
}
this.visibleCount++
return index
}
// 释放实例
public releaseInstance(index: number): void {
if (index < 0 || index >= this.maxCount) {
console.error(`Invalid instance index: ${index}`)
return
}
// 将实例移到屏幕外
this.setInstanceMatrix(index, new THREE.Matrix4().setPosition(0, -10000, 0))
this.freeIndices.push(index)
this.visibleCount--
}
// 设置实例的变换矩阵
public setInstanceMatrix(index: number, matrix: THREE.Matrix4): void {
matrix.toArray(this.matrixArray, index * 16)
this.needsUpdate = true
}
// 更新所有实例
public update(): void {
if (!this.needsUpdate) return
// 高效更新所有矩阵
if (this.mesh.instanceMatrix) {
this.mesh.instanceMatrix.needsUpdate = true
}
// 使用矩阵纹理优化(可选)
if (!this.matrixTexture) {
this.matrixTexture = new THREE.DataTexture(
this.matrixArray,
4, // 每行4个矩阵
this.maxCount,
THREE.RGBAFormat,
THREE.FloatType
)
this.matrixTexture.needsUpdate = true
// 在着色器中使用矩阵纹理
if (Array.isArray(this.mesh.material)) {
this.mesh.material.forEach(mat => {
mat.onBeforeCompile = shader => {
this.applyMatrixTextureShader(shader)
}
})
} else {
this.mesh.material.onBeforeCompile = shader => {
this.applyMatrixTextureShader(shader)
}
}
} else {
this.matrixTexture.needsUpdate = true
}
this.needsUpdate = false
}
// 获取实例化网格
public getMesh(): THREE.InstancedMesh {
return this.mesh
}
// 重置所有实例
private resetAllInstances(): void {
const hiddenMatrix = new THREE.Matrix4().setPosition(0, -10000, 0)
for (let i = 0; i < this.maxCount; i++) {
hiddenMatrix.toArray(this.matrixArray, i * 16)
this.freeIndices.push(i)
}
this.mesh.instanceMatrix.needsUpdate = true
}
// 应用矩阵纹理着色器修改
private applyMatrixTextureShader(shader: THREE.WebGLProgramParametersWithUniforms): void {
shader.uniforms.instanceMatrixTexture = { value: this.matrixTexture }
shader.vertexShader = `
uniform sampler2D instanceMatrixTexture;
varying vec4 vInstancePosition;
mat4 getInstanceMatrix(float index) {
vec2 texCoord = vec2(mod(index, 4.0) * 0.25, floor(index * 0.25) / ${this.maxCount.toFixed(1)});
vec4 row1 = texture2D(instanceMatrixTexture, texCoord);
vec4 row2 = texture2D(instanceMatrixTexture, texCoord + vec2(0.25, 0.0));
vec4 row3 = texture2D(instanceMatrixTexture, texCoord + vec2(0.5, 0.0));
vec4 row4 = texture2D(instanceMatrixTexture, texCoord + vec2(0.75, 0.0));
return mat4(row1, row2, row3, row4);
}
` + shader.vertexShader
shader.vertexShader = shader.vertexShader.replace(
'#include <begin_vertex>',
`
float instanceIndex = float(gl_InstanceID);
mat4 instanceMatrix = getInstanceMatrix(instanceIndex);
vec3 transformed = (instanceMatrix * vec4(position, 1.0)).xyz;
vInstancePosition = instanceMatrix * vec4(position, 1.0);
`
)
}
}

50
src/core/manager/InteractionManager.ts

@ -0,0 +1,50 @@
import type Viewport from '@/core/engine/Viewport.ts'
import { watch } from 'vue'
import type IControls from '@/core/controls/IControls.ts'
import type BaseInteraction from '@/core/base/BaseInteraction.ts'
import { getInteraction } from '@/core/manager/ModuleManager.ts'
import * as THREE from 'three'
/**
*
*/
export default class InteractionManager implements IControls {
private viewport: Viewport
/**
*
*/
currentTool: BaseInteraction | null = null
//搭配 state.cursorMode = xxx 之后, currentTool.start(第一个参数) 使用
toolStartObject: THREE.Object3D | null = null
init(viewport: Viewport) {
this.viewport = viewport
this.viewport.watchList.push(watch(() => this.viewport.state.cursorMode, (newVal: CursorMode) => {
const state = this.viewport.state
if (!state.isReady) {
return
}
if (this.currentTool) {
this.currentTool.stop()
this.currentTool = null
}
if (newVal === 'normal' || !newVal) {
this.viewport.dragControl.dragControls.enabled = true
return
}
this.currentTool = getInteraction(newVal)
this.viewport.dragControl.dragControls.enabled = false
this.currentTool.start(this.viewport, this.toolStartObject)
this.toolStartObject = null
}))
}
destory() {
}
}

82
src/core/manager/ModuleManager.ts

@ -0,0 +1,82 @@
import * as THREE from 'three'
import BaseRenderer from '@/core/base/BaseRenderer'
import BaseInteraction from '@/core/base/BaseInteraction'
import type { IMeta } from '@/core/base/IMeta'
import BaseEntity from '@/core/base/BaseItemEntity'
// Define the ModuleDefineOption interface
export interface ModuleDefineOption {
/**
*
*/
name: string;
renderer: BaseRenderer;
interaction: BaseInteraction;
meta: IMeta;
entity: new () => BaseEntity;
}
// Internal storage for module definitions
const modules = new Map<string, ModuleDefineOption>()
window['modules'] = modules
/**
*
*/
export function defineModule(option: ModuleDefineOption): void {
if (modules.has(option.name)) {
throw new Error(`Module with name "${option.name}" is already defined.`)
}
modules.set(option.name, option)
}
/**
*
*
*/
export function getModuleOption(name: string): ModuleDefineOption {
const module = modules.get(name)
if (!module) {
throw new Error(`Module with name "${name}" is not defined.`)
}
return module
}
/**
* ,
*
*/
export function getRenderer<T extends BaseRenderer>(name: string): T {
const module = getModuleOption(name)
return module.renderer as T
}
/**
* ,
*
*/
export function getInteraction<T extends BaseInteraction>(name: string): T {
const module = getModuleOption(name)
return module.interaction as T
}
/**
* ,
*
*/
export function getMeta<T extends IMeta>(name: string): T {
const module = getModuleOption(name)
return module.meta as T
}
/**
* ,
*
*/
export function createEntity<T extends BaseEntity>(name: string, itemjson: ItemJson, objects: THREE.Object3D[]): T {
const module = getModuleOption(name)
const v = new module.entity() as T
v.setItem(itemjson)
v.setObjects(objects)
return v
}

72
src/designer/StateManager.ts → src/core/manager/StateManager.ts

@ -1,15 +1,13 @@
import _ from 'lodash'
import localforage from 'localforage'
import type Viewport from '@/designer/Viewport.ts'
import type Viewport from '@/core/engine/Viewport.ts'
import { markRaw, reactive, ref } from 'vue'
import type { VData, VDataItem } from '@/types/Types'
/**
* THREE.js .
* threejs
* ,
* 1. ,
* 2.
* 3.
* 3. Interaction
* - 1. beginUserWrite
* - 2. vdata
* - 3. endUserWrite
@ -45,29 +43,29 @@ import type { VData, VDataItem } from '@/types/Types'
*/
export default class StateManager {
/**
*
* , key
*/
isChanged = ref(false)
readonly id: string
/**
*
* ,
*/
isLoading = ref(false)
readonly viewport: Viewport
/**
*
*
*/
vdata: VData = { items: [], isChanged: false }
readonly isChanged = ref(false)
/**
* , key
*
*/
id: string
readonly isLoading = ref(false)
/**
* ,
*
*/
viewport: Viewport
vdata: VData
/**
* 使
@ -102,7 +100,8 @@ export default class StateManager {
*/
constructor(id: string, viewport: Viewport, bufferSize = 50) {
this.id = id
this.viewport = markRaw(viewport)
this.viewport = viewport
this.historyBufferSize = bufferSize
// 初始化固定大小的历史缓冲区
this.historySteps = new Array(this.maxHistorySteps).fill(null)
@ -240,7 +239,7 @@ export default class StateManager {
/**
*
*/
async load(items: VDataItem[]) {
async load(data: VData) {
this.isLoading.value = true
this.historySteps = new Array(this.maxHistorySteps).fill(null)
this.historyIndex = -1
@ -250,7 +249,12 @@ export default class StateManager {
this.stopAutoSave()
// 直接替换数组引用(避免响应式开销)
this.vdata.items = items
this.vdata = {
id: this.id,
items: data.items,
isChanged: false,
catalog: data.catalog
}
this.fullSync() // 同步到视口
// 初始状态作为第一步
@ -264,7 +268,7 @@ export default class StateManager {
await this.saveToLocalstore()
this.pendingChanges = false
console.log('[StateManager] 加载完成,共 ', items.length, '个对象')
console.log('[StateManager] 加载完成,共 ', data.items.length, '个对象')
} finally {
this.isLoading.value = false
@ -424,10 +428,16 @@ export default class StateManager {
// }
// }
/**
* indexDb()
*/
async saveToLocalstore() {
await localforage.setItem(`scene-tmp-${this.id}`, this.vdata)
}
/**
*
*/
async loadFromLocalstore() {
try {
this.isLoading.value = true
@ -480,21 +490,21 @@ export default class StateManager {
*
*/
startAutoSave() {
if (this.autoSaveInterval) return
this.autoSaveInterval = window.setInterval(() => {
this.autoSaveIfNeeded()
}, this.autoSaveIntervalMs)
// if (this.autoSaveInterval) return
//
// this.autoSaveInterval = window.setInterval(() => {
// this.autoSaveIfNeeded()
// }, this.autoSaveIntervalMs)
}
/**
*
*/
stopAutoSave() {
if (this.autoSaveInterval) {
clearInterval(this.autoSaveInterval)
this.autoSaveInterval = null
}
// if (this.autoSaveInterval) {
// clearInterval(this.autoSaveInterval)
// this.autoSaveInterval = null
// }
}
/**
@ -519,11 +529,9 @@ export default class StateManager {
*/
destroy() {
this.stopAutoSave()
this.removeLocalstore().catch(console.error)
// 清理引用
this.viewport = null as any
this.vdata.items = []
this.historySteps = []
delete this.vdata
delete this.historySteps
}
/**

180
src/core/manager/WorldModel.ts

@ -0,0 +1,180 @@
import _ from 'lodash'
import { reactive, watch } from 'vue'
import EventBus from '@/runtime/EventBus'
export interface WorldModelState {
isOpened: boolean // 是否已打开世界模型
catalog: Catalog // 世界模型目录数据
catalogCode: string // 当前楼层的目录代码
stateManagerId: string // 当前楼层的状态管理器id
}
/**
*
*/
export default class WorldModel {
data: any = null
/**
*
*/
state: WorldModelState = reactive({
isOpened: false, // 是否已打开世界模型
catalogCode: '',
stateManagerId: '', // 当前楼层的状态管理器id
catalog: [] as Catalog // 世界模型目录数据
})
get gridOption(): IGridHelper {
const data = _.get(this.data, 'Tool.gridHelper')
return _.defaultsDeep(data, {
axesEnabled: true,
axesSize: 1000,
axesDivisions: 4,
axesColor: 0x000000,
axesOpacity: 1,
gridEnabled: true, // 启用网格
gridSize: 1000, // 网格大小, 单位米
gridDivisions: 1000, // 网格分割数
gridColor: 0x999999, // 网格颜色, 十六进制颜色值
gridOpacity: 0.8, // 网格透明度
snapEnabled: true, // 启用吸附
snapDistance: 0.25 // 吸附距离, 单位米
})
}
constructor() {
}
init() {
// 观察 this.state.catalogCode 的变化, 如果变化就调用 catalogCodeChange 方法
watch(() => this.state.catalogCode, (newValue, oldValue) => {
worldModel.loadFloor(newValue)
})
return Promise.all([
import('@/modules/measure')
]).then(() => {
console.log('世界模型初始化完成')
})
}
/**
*
*/
loadCatalog(data: any) {
this.data = data
this.state.catalog = data.catalog
this.state.isOpened = true
}
/**
* , +id
*/
loadFloor(catalogCode: string) {
if (!catalogCode) {
this.state.catalogCode = ''
this.state.stateManagerId = ''
EventBus.dispatch('catalogChanged', {
catalogCode: this.state.catalogCode,
stateManagerId: this.state.stateManagerId
})
return
}
const floor = _.find(this.data.items, r => r.catalogCode === catalogCode && r.t === 'floor')
if (!floor) {
system.msg('楼层不存在: ' + catalogCode)
return
}
this.state.catalogCode = catalogCode
this.state.stateManagerId = this.data.project_uuid + '_' + catalogCode
EventBus.dispatch('catalogChanged', {
catalogCode: this.state.catalogCode,
stateManagerId: this.state.stateManagerId
})
}
// loadFloorToScene(viewport: Viewport, scene: THREE.Scene, levelCode: string) {
// let floor = _.find(this.data.items, r => r.name === levelCode && r.t === 'floor')
// if (!floor) {
// console.info(`新建楼层: ${levelCode}`)
//
// if (!_.isArray(this.data.items)) {
// this.data.items = []
// }
// floor = { name: levelCode, t: 'floor', items: [] }
// this.data.items.push(floor)
// }
//
// loadSceneFromJson(viewport, scene, floor.items)
// }
// open() {
// if (this.sceneMap.size > 0) {
// // 释放旧场景
// this.sceneMap.forEach((scene: Scene) => {
// this.sceneDispose(scene)
// })
// }
// if (this.viewPorts.length > 0) {
// // 注销视口
// this.viewPorts.forEach((viewport: Viewport) => {
// this.unregisterViewport(viewport)
// })
// }
//
// system.msg('打开世界地图完成')
// this.data = markRaw(Example1)
// this.state.openFileName = 'example1'
// this.state.allLevels = reactive(this.data.allLevels)
// }
// /**
// * 获取当前楼层的场景, 如果没有则创建一个新的场景
// */
// getSceneByFloor(viewport: Viewport, floor: string) {
// if (this.sceneMap.has(floor)) {
// return this.sceneMap.get(floor)
// } else {
// const scene = this.createScene(viewport, floor)
//
// this.sceneMap.set(floor, scene)
// return scene
// }
// }
//
// /**
// * 创建一个新的场景
// */
// createScene(viewport: Viewport, floor: string) {
// const scene = new Scene()
// scene.background = new THREE.Color(0xeeeeee)
//
// this.loadFloorToScene(viewport, scene, floor)
// return scene
// }
// /**
// * 注册视口
// */
// registerViewport(viewport: Viewport) {
// this.viewPorts = this.viewPorts || []
// this.viewPorts.push(viewport)
// }
//
// /**
// * 注销视口
// */
// unregisterViewport(viewport: Viewport) {
// const index = this.viewPorts.indexOf(viewport)
// if (index > -1) {
// this.viewPorts.splice(index, 1)
// }
// }
}

8
src/designer/ModelView.vue

@ -1,8 +0,0 @@
<template>
<div class="section-canvas">
<div class="section-content"></div>
</div>
</template>
<script>
export default {}
</script>

73
src/designer/model2DEditor/Model2DEditor.vue

@ -1,73 +0,0 @@
<template>
<div class="section-canvas">
<div class="section-top-toolbar section-toolbar">
<span class="section-toolbar-line" style="margin-left: 85px;"></span>
<el-button :icon="renderIcon('antd ClusterOutlined')" link></el-button>
<span class="section-toolbar-line"></span>
<el-cascader placeholder="选择楼层" size="small" v-model="currentFloor"
:options="allLevels" filterable :show-all-levels="false" clearable
:props="{emitPath:false}" />
</div>
<div class="section-content">
<div v-if="currentFloor" :key="currentFloor"
class="canvas-container" ref="canvasContainer" tabindex="1" />
</div>
<div class="section-bottom-toolbar section-toolbar" v-if="!!state">
<div class="section-toolbar-left">
<el-button title="鼠标状态 (ESC)" :icon="renderIcon('fa MousePointer')" link
:type="state?.cursorMode===Constract.CursorModeNormal?'primary':''"
@click="()=>state.cursorMode = Constract.CursorModeNormal"></el-button>
<span class="section-toolbar-line"></span>
<el-button title="框选模式 (T)" :icon="renderIcon('FullScreen')" link
:type="state?.cursorMode===Constract.CursorModeSelectByRec?'primary':''"
@click="()=>state.cursorMode = Constract.CursorModeSelectByRec"></el-button>
<span class="section-toolbar-line"></span>
<el-button title="物理流动线 (Z)" :icon="renderIcon('antd EnterOutlined')" link
:type="state?.cursorMode===Constract.CursorModeALink?'primary':''"
@click="()=>state.cursorMode = Constract.CursorModeALink"></el-button>
<span class="section-toolbar-line"></span>
<el-button title="逻辑关联 (X)" :icon="renderIcon('antd LinkOutlined')" link
:type="state?.cursorMode===Constract.CursorModeSLink?'primary':''"
@click="()=>state.cursorMode = Constract.CursorModeSLink"></el-button>
<span class="section-toolbar-line"></span>
<el-button title="测量工具" :icon="renderIcon('fa Ruler')" link
:type="state?.cursorMode===Constract.CursorModeMeasure?'primary':''"
@click="()=>state.cursorMode = Constract.CursorModeMeasure"></el-button>
<span class="section-toolbar-line"></span>
<el-button title="输送线" :icon="renderIcon('fa Line')" link
:type="state?.cursorMode===Constract.CursorModeConveyor?'primary':''"
@click="()=>state.cursorMode = Constract.CursorModeConveyor"></el-button>
</div>
<div class="section-toolbar-right">
<el-input v-model="searchKeyword" size="small" style="width: 110px; margin-right: 5px;"
placeholder="Search">
<template #prefix>
<component :is="renderIcon('element Search')"></component>
</template>
</el-input>
<el-text type="warning">00001</el-text>
<span class="section-toolbar-line"></span>
<el-text type="danger">00011</el-text>
<span class="section-toolbar-line"></span>
<div>
{{ toFixed(state?.camera.position.x) }},
{{ toFixed(state?.camera.position.y) }},
{{ toFixed(state?.camera.position.z) }}
</div>
<span class="section-toolbar-line"></span>
<div>
{{ toFixed(state?.mouse.x) }},{{ toFixed(state?.mouse.z) }}
</div>
</div>
</div>
</div>
</template>
<script>
import Model2DEditorJs from './Model2DEditorJs.js'
export default Model2DEditorJs
</script>

91
src/designer/model2DEditor/Model2DEditorJs.js

@ -1,91 +0,0 @@
import * as THREE from 'three'
import { renderIcon } from '@/utils/webutils.ts'
import { defineComponent, markRaw } from 'vue'
import Viewport from '@/designer/Viewport.ts'
import Constract from '@/designer/Constract.js'
import IWidgets from '@/designer/viewWidgets/IWidgets.js'
export default defineComponent({
name: 'Model2DEditor',
mixins: [IWidgets],
emits: ['viewportChanged'],
data() {
return {
Constract,
isReady: false,
viewport: null,
currentFloor: '',
searchKeyword: ''
}
},
mounted() {
},
beforeMount() {
this.initByFloor('')
},
methods: {
renderIcon,
toFixed(num) {
if (num === undefined || num === null) {
return ''
}
if (isNaN(num)) {
return num
}
return parseFloat(num).toFixed(2)
},
initByFloor(floor) {
this.isReady = false
const viewportOrigin = this.viewport
if (viewportOrigin && viewportOrigin.state.isReady) {
viewportOrigin.destroy()
}
delete window['editor']
delete window['viewport']
delete window['scene']
delete window['renderer']
delete window['camera']
delete window['renderer']
delete window['controls']
if (!floor) {
return
}
const viewerDom = this.$refs.canvasContainer
const viewport = markRaw(new Viewport(worldModel))
this.viewport = viewport
viewport.initThree(viewerDom, floor)
window['viewport'] = viewport
window['THREE'] = THREE
window['scene'] = viewport.scene
window['renderer'] = viewport.renderer
window['camera'] = viewport.camera
window['renderer'] = viewport.renderer
window['controls'] = viewport.controls
viewerDom.focus()
this.$emit('viewportChanged', viewport)
this.isReady = true
}
},
watch: {
currentFloor: {
handler(newVal, oldVal) {
const floor = newVal
this.$nextTick(() => {
console.log('floor changed', floor)
this.initByFloor(newVal)
})
}
}
},
computed: {
allLevels() {
return worldModel.state.allLevels
}
}
})

213
src/editor/Model2DEditor.vue

@ -0,0 +1,213 @@
<template>
<div class="section-canvas">
<div class="section-top-toolbar section-toolbar">
<span class="section-toolbar-line" style="margin-left: 85px;"></span>
<el-button :icon="renderIcon('antd ClusterOutlined')" link></el-button>
<span class="section-toolbar-line"></span>
<el-cascader placeholder="选择楼层" size="small" v-model="currentLevel"
:options="calcCatalog" filterable :show-all-levels="false" clearable
:props="{emitPath:false}" />
</div>
<div class="section-content">
<div v-if="currentStateManagerId" :key="currentStateManagerId"
class="canvas-container" ref="canvasContainer" tabindex="1" />
</div>
<div class="section-bottom-toolbar section-toolbar" v-if="!!state">
<div class="section-toolbar-left">
<el-button title="鼠标状态 (ESC)" :icon="renderIcon('fa MousePointer')" link
:type="state?.cursorMode===Constract.CursorModeNormal?'primary':''"
@click="()=>state.cursorMode = Constract.CursorModeNormal"></el-button>
<span class="section-toolbar-line"></span>
<el-button title="框选模式 (T)" :icon="renderIcon('FullScreen')" link
:type="state?.cursorMode===Constract.CursorModeSelectByRec?'primary':''"
@click="()=>state.cursorMode = Constract.CursorModeSelectByRec"></el-button>
<span class="section-toolbar-line"></span>
<el-button title="物理流动线 (Z)" :icon="renderIcon('antd EnterOutlined')" link
:type="state?.cursorMode===Constract.CursorModeALink?'primary':''"
@click="()=>state.cursorMode = Constract.CursorModeALink"></el-button>
<span class="section-toolbar-line"></span>
<el-button title="逻辑关联 (X)" :icon="renderIcon('antd LinkOutlined')" link
:type="state?.cursorMode===Constract.CursorModeSLink?'primary':''"
@click="()=>state.cursorMode = Constract.CursorModeSLink"></el-button>
<span class="section-toolbar-line"></span>
<el-button title="测量工具" :icon="renderIcon('fa Ruler')" link
:type="state?.cursorMode===Constract.CursorModeMeasure?'primary':''"
@click="()=>state.cursorMode = Constract.CursorModeMeasure"></el-button>
<span class="section-toolbar-line"></span>
<el-button title="输送线" :icon="renderIcon('fa Line')" link
:type="state?.cursorMode===Constract.CursorModeConveyor?'primary':''"
@click="()=>state.cursorMode = Constract.CursorModeConveyor"></el-button>
</div>
<div class="section-toolbar-right">
<el-input v-model="searchKeyword" size="small" style="width: 110px; margin-right: 5px;"
placeholder="Search">
<template #prefix>
<component :is="renderIcon('element Search')"></component>
</template>
</el-input>
<el-text type="warning">00001</el-text>
<span class="section-toolbar-line"></span>
<el-text type="danger">00011</el-text>
<span class="section-toolbar-line"></span>
<div>
{{ toFixed(state?.camera.position.x) }},
{{ toFixed(state?.camera.position.y) }},
{{ toFixed(state?.camera.position.z) }}
</div>
<span class="section-toolbar-line"></span>
<div>
{{ toFixed(state?.mouse.x) }},{{ toFixed(state?.mouse.z) }}
</div>
</div>
</div>
</div>
</template>
<script>
import * as THREE from 'three'
import { getQueryParams, renderIcon, setQueryParam } from '@/utils/webutils'
import { defineComponent, markRaw } from 'vue'
import Viewport from '@/core/engine/Viewport'
import Constract from '@/core/Constract'
import EventBus from '@/runtime/EventBus'
import SceneHelp from '@/core/engine/SceneHelp'
export default defineComponent({
name: 'Model2DEditor',
emits: ['viewportChanged'],
data() {
return {
Constract,
isReady: false,
scene: null,
viewport: null,
currentStateManagerId: null,
searchKeyword: ''
}
},
mounted() {
EventBus.on('catalogChanged', (floor) => {
// ,
this.initByFloor()
})
},
beforeUnmount() {
this.destroyScene()
},
methods: {
renderIcon,
destroyScene() {
if (this.viewport) {
this.viewport.destroy()
this.viewport = null
}
if (this.scene) {
this.scene.destory()
this.scene = null
}
this.isReady = false
},
toFixed(num) {
if (num === undefined || num === null) {
return ''
}
if (isNaN(num)) {
return num
}
return parseFloat(num).toFixed(2)
},
initByFloor() {
// url ?store=${stateManager.id}
// if (stateManager) {
// window.history.replaceState({}, '', window.location.href + '?store=' + stateManager.id)
// }
// const stateManager = new StateManager(id, 50)
// worldModel.stateManager = stateManager
// worldModel.stateManager = stateManager
// system.showLoading()
// stateManager.load(floor.items)
// .finally(() => {
// system.clearLoading()
// })
// return stateManager
this.destroyScene()
delete window['editor']
delete window['viewport']
delete window['scene']
delete window['renderer']
delete window['camera']
delete window['renderer']
delete window['controls']
const id = worldModel.state.stateManagerId
this.currentStateManagerId = id
if (!worldModel.state.catalogCode || !worldModel.state.isOpened || !id) {
//
setQueryParam('store', id)
return
}
// canvasContainer
this.$nextTick(() => {
const viewerDom = this.$refs.canvasContainer
const sceneHelp = new SceneHelp(worldModel, worldModel.state.catalogCode)
const viewport = new Viewport(sceneHelp, viewerDom)
this.scene = markRaw(sceneHelp)
this.viewport = markRaw(viewport)
viewport.initThree({ stateManagerId: id })
setQueryParam('store', id)
// window.history.replaceState({}, '', window.location.href + '?store=' + id)
window['viewport'] = viewport
window['THREE'] = THREE
window['scene'] = sceneHelp.scene
window['renderer'] = viewport.renderer
window['camera'] = viewport.camera
window['renderer'] = viewport.renderer
window['controls'] = viewport.controls
viewerDom.focus()
//
this.$emit('viewportChanged', viewport)
this.isReady = true
})
}
},
computed: {
state() {
return this.viewport?.state
},
currentLevel: {
get() {
return worldModel.state.catalogCode
},
set(newVal) {
worldModel.state.catalogCode = newVal
}
},
calcCatalog() {
if (!worldModel.state.catalog || !worldModel.state.isOpened) {
return []
}
return worldModel.state.catalog.map(group => ({
value: group.label,
label: group.label,
children: group.items.map(item => ({
value: item.catalogCode,
label: item.label
}))
}))
}
}
})
</script>

8
src/editor/Model3DViewer.vue

@ -0,0 +1,8 @@
<template>
Model3DViewer not implemented yet.
</template>
<script>
export default {
}
</script>

0
src/views/ModelMain.less → src/editor/ModelMain.less

22
src/views/ModelMain.vue → src/editor/ModelMain.vue

@ -15,7 +15,7 @@
</span>
</div>
</div>
<div class="app-section">
<div class="app-section" v-if="worldModel.state.isOpened">
<div class="btns-toolbar btns-toolbar-left">
<div class="btns btns-top">
<template v-for="panel in getWidgetBySide('left')">
@ -56,7 +56,7 @@
<Model2DEditor @viewportChanged="setCurrentViewport" />
</el-tab-pane>
<el-tab-pane label="3D视图" name="ModelView" lazy>
<ModelView />
<Model3DViewer />
</el-tab-pane>
<el-tab-pane label="模型属性" name="ModelFile" lazy>
<el-empty description="暂无数据" />
@ -102,23 +102,22 @@
</div>
</template>
<script>
import Logo from '@/assets/images/logo.png'
import './ModelMain.less'
import { markRaw } from 'vue'
import { renderIcon } from '@/utils/webutils.js'
import Split from '@/components/split/split.vue'
import SplitArea from '@/components/split/split-area.vue'
import { ModelMainInit, ModelMainMounted, ModelMainUnmounted } from '@/views/ModelMainInit.js'
import { ModelMainInit, ModelMainMounted, ModelMainUnmounted } from './ModelMainInit.js'
import { getRootMenu } from '@/runtime/DefineMenu.js'
import { getWidgetByName, getWidgetBySide, getAllWidget } from '@/runtime/DefineWidget.js'
import Model2DEditor from '@/designer/model2DEditor/Model2DEditor.vue'
import ModelView from '@/designer/ModelView.vue'
import Model2DEditor from './Model2DEditor.vue'
import Model3DViewer from './Model3DViewer.vue'
import { normalizeShortKey } from '@/utils/webutils.ts'
import { markRaw } from 'vue'
import Logo from '@/assets/images/logo.png'
import './ModelMain.less'
export default {
components: { Model2DEditor, ModelView, Split, SplitArea },
components: { Model2DEditor, Model3DViewer, Split, SplitArea },
created() {
ModelMainInit()
},
@ -176,6 +175,9 @@ export default {
}
},
computed: {
worldModel() {
return window['worldModel']
},
calcLeftPanel() {
if (!this.sectionLeftName || this.hideLeft) {
return undefined

34
src/views/ModelMainInit.ts → src/editor/ModelMainInit.ts

@ -1,21 +1,21 @@
import _ from 'lodash'
import hotkeys from 'hotkeys-js'
import AlarmMeta from '@/designer/viewWidgets/alarm/AlarmMeta'
import LoggerMeta from '@/designer/viewWidgets/logger/LoggerMeta'
import ModeltreeMeta from '@/designer/viewWidgets/modeltree/ModeltreeMeta'
import MonitorMeta from '@/designer/viewWidgets/monitor/MonitorMeta'
import PropertyMeta from '@/designer/viewWidgets/property/PropertyMeta'
import ScriptMeta from '@/designer/viewWidgets/script/ScriptMeta'
import TaskMeta from '@/designer/viewWidgets/task/TaskMeta'
import ToolboxMeta from '@/designer/viewWidgets/toolbox/ToolboxMeta'
import AlarmMeta from './widgets/alarm/AlarmMeta'
import LoggerMeta from './widgets/logger/LoggerMeta'
import ModeltreeMeta from './widgets/modeltree/ModeltreeMeta'
import MonitorMeta from './widgets/monitor/MonitorMeta'
import PropertyMeta from './widgets/property/PropertyMeta'
import ScriptMeta from './widgets/script/ScriptMeta'
import TaskMeta from './widgets/task/TaskMeta'
import ToolboxMeta from './widgets/toolbox/ToolboxMeta'
import FileMenu from '@/designer/menus/FileMenu.ts'
import EditMenu from '@/designer/menus/EditMenu.ts'
import ToolsMenu from '@/designer/menus/Tools.ts'
import Model3DView from '@/designer/menus/Model3DView.ts'
import { forEachMenu } from '@/runtime/DefineMenu.ts'
import { normalizeShortKey } from '@/utils/webutils.ts'
import WorldModel from '@/model/WorldModel.ts'
import FileMenu from './menus/FileMenu'
import EditMenu from './menus/EditMenu'
import ToolsMenu from './menus/Tools'
import Model3DView from './menus/Model3DView'
import { forEachMenu } from '@/runtime/DefineMenu'
import { normalizeShortKey } from '@/utils/webutils'
import WorldModel from '@/core/manager/WorldModel'
/**
*
@ -56,9 +56,7 @@ export function ModelMainMounted() {
}
})
return worldModel.init().then(() => {
worldModel.open()
})
return worldModel.init()
}
export function ModelMainUnmounted() {

2
src/designer/menus/EditMenu.ts → src/editor/menus/EditMenu.ts

@ -1,7 +1,7 @@
import { renderIcon } from '@/utils/webutils.ts'
import { defineMenu } from '@/runtime/DefineMenu.ts'
import SvgCode from '@/components/icons/SvgCode'
import { escByKeyboard, quickCopyByMouse, deletePointByKeyboard } from '@/model/ModelUtils.ts'
import { escByKeyboard, quickCopyByMouse, deletePointByKeyboard } from '@/core/ModelUtils'
export default defineMenu((menus) => {
menus.insertChildren('modelFile',

9
src/designer/menus/FileMenu.ts → src/editor/menus/FileMenu.ts

@ -1,7 +1,6 @@
import { renderIcon } from '@/utils/webutils.ts'
import { defineMenu } from '@/runtime/DefineMenu.ts'
import SvgCode from '@/components/icons/SvgCode'
import { Open } from '@element-plus/icons-vue'
export default defineMenu((menus) => {
menus.insertChildren('file',
@ -12,7 +11,13 @@ export default defineMenu((menus) => {
{
name: 'open', label: '打开', icon: SvgCode.open, order: 1, tip: 'Ctrl+O',
click: () => {
system.msg('打开模型文件')
system.showLoading()
import('@/example/example1').then(res => {
worldModel.loadCatalog(res.default)
}).finally(() => {
system.clearLoading()
})
}
},
{

2
src/designer/menus/Model3DView.ts → src/editor/menus/Model3DView.ts

@ -1,5 +1,5 @@
import { defineMenu } from '@/runtime/DefineMenu.ts'
import Model3DView from '@/designer/model3DView/Model3DView.vue'
import Model3DView from '@/components/Model3DView.vue'
export default defineMenu((menus) => {
menus.insertChildren('tool',

0
src/designer/menus/Tools.ts → src/editor/menus/Tools.ts

0
src/designer/metaComponents/ColorItem.vue → src/editor/propEditors/ColorItem.vue

6
src/designer/metaComponents/IMetaProp.ts → src/editor/propEditors/IMetaProp.ts

@ -1,7 +1,7 @@
import * as THREE from 'three'
import { type ItemTypeMetaItem } from '@/model/itemType/ItemTypeDefine.ts'
import { type ItemTypeMetaItem } from '@/model/itemType/ItemTypeDefine'
import { defineComponent, type PropType } from 'vue'
import type Viewport from '@/designer/Viewport.ts'
import type Viewport from '@/core/engine/Viewport'
import EventBus from '@/runtime/EventBus'
export default defineComponent({
@ -10,7 +10,7 @@ export default defineComponent({
viewport: Object as PropType<Viewport>
},
mounted() {
EventBus.$on('objectChanged', (data) => {
EventBus.on('objectChanged', (data) => {
//@ts-ignore
if (typeof this.refreshValue === 'function') {
//@ts-ignore

0
src/designer/metaComponents/NumberInput.vue → src/editor/propEditors/NumberInput.vue

0
src/designer/metaComponents/SwitchItem.vue → src/editor/propEditors/SwitchItem.vue

0
src/designer/metaComponents/TextInput.vue → src/editor/propEditors/TextInput.vue

0
src/designer/metaComponents/Transform.vue → src/editor/propEditors/Transform.vue

0
src/designer/metaComponents/UUIDItem.vue → src/editor/propEditors/UUIDItem.vue

6
src/designer/viewWidgets/IWidgets.ts → src/editor/widgets/IWidgets.ts

@ -1,6 +1,6 @@
import { defineComponent } from 'vue'
import { renderIcon } from '@/utils/webutils.js'
import Viewport, { type ViewportState } from '@/designer/Viewport.ts'
import Viewport, { type ViewportState } from '@/core/engine/Viewport.ts'
export type IWidgetData = {
/**
@ -26,10 +26,10 @@ export default defineComponent({
}
},
emits: ['close'],
data() {
data(): IWidgetData {
return {
isActivated: false
} as IWidgetData
}
},
methods: {
renderIcon,

0
src/designer/viewWidgets/alarm/AlarmMeta.ts → src/editor/widgets/alarm/AlarmMeta.ts

0
src/designer/viewWidgets/alarm/AlarmView.vue → src/editor/widgets/alarm/AlarmView.vue

0
src/designer/viewWidgets/logger/LoggerMeta.ts → src/editor/widgets/logger/LoggerMeta.ts

0
src/designer/viewWidgets/logger/LoggerView.vue → src/editor/widgets/logger/LoggerView.vue

0
src/designer/viewWidgets/modeltree/ModeltreeMeta.ts → src/editor/widgets/modeltree/ModeltreeMeta.ts

4
src/designer/viewWidgets/modeltree/ModeltreeView.vue → src/editor/widgets/modeltree/ModeltreeView.vue

@ -5,7 +5,9 @@
<component :is="renderIcon('element Search')"></component>
</template>
</el-input>
<el-cascader placeholder="选择楼层" size="small" v-model="currentLevel" :options="allLevels" filterable
<el-cascader placeholder="选择楼层" size="small" v-model="currentLevel" :options="calcCatalog"
filterable :show-all-levels="false" clearable
:props="{emitPath:false}"
style="margin-right: 5px; width: 150px;" />
</div>
<div class="calc-left-panel">

23
src/designer/viewWidgets/modeltree/ModeltreeViewJs.js → src/editor/widgets/modeltree/ModeltreeViewJs.js

@ -8,7 +8,6 @@ export default defineComponent({
mixins: [IWidgets],
data() {
return {
currentLevel: '',
searchKeyword: '',
treedata: data
}
@ -34,8 +33,26 @@ export default defineComponent({
}
},
computed: {
allLevels() {
return worldModel.state.allLevels
currentLevel: {
get() {
return worldModel.state.catalogCode
},
set(newVal) {
worldModel.state.catalogCode = newVal
}
},
calcCatalog() {
if (!worldModel.state.catalog || !worldModel.state.isOpened) {
return []
}
return worldModel.state.catalog.map(group => ({
value: group.label,
label: group.label,
children: group.items.map(item => ({
value: item.catalogCode,
label: item.label
}))
}))
}
}
})

0
src/designer/viewWidgets/monitor/MonitorMeta.ts → src/editor/widgets/monitor/MonitorMeta.ts

0
src/designer/viewWidgets/monitor/MonitorView.vue → src/editor/widgets/monitor/MonitorView.vue

0
src/designer/viewWidgets/property/PropertyMeta.ts → src/editor/widgets/property/PropertyMeta.ts

12
src/designer/viewWidgets/property/PropertyView.vue → src/editor/widgets/property/PropertyView.vue

@ -45,12 +45,12 @@
</template>
<script>
import IWidgets from '../IWidgets.js'
import TextInput from '@/designer/metaComponents/TextInput.vue'
import Transform from '@/designer/metaComponents/Transform.vue'
import SwitchItem from '@/designer/metaComponents/SwitchItem.vue'
import ColorItem from '@/designer/metaComponents/ColorItem.vue'
import UUIDItem from '@/designer/metaComponents/UUIDItem.vue'
import NumberInput from '@/designer/metaComponents/NumberInput.vue'
import TextInput from '../../propEditors/TextInput.vue'
import Transform from '../../propEditors/Transform.vue'
import SwitchItem from '../../propEditors/SwitchItem.vue'
import ColorItem from '../../propEditors/ColorItem.vue'
import UUIDItem from '../../propEditors/UUIDItem.vue'
import NumberInput from '../../propEditors/NumberInput.vue'
export default {
name: 'PropertyView',

0
src/designer/viewWidgets/script/ScriptMeta.ts → src/editor/widgets/script/ScriptMeta.ts

2
src/designer/viewWidgets/script/ScriptView.vue → src/editor/widgets/script/ScriptView.vue

@ -15,7 +15,7 @@
</div>
</template>
<script>
import IWidgets from '@/designer/viewWidgets/IWidgets.js'
import IWidgets from '../IWidgets.js'
export default {
name: 'ScriptView',

0
src/designer/viewWidgets/task/TaskMeta.ts → src/editor/widgets/task/TaskMeta.ts

0
src/designer/viewWidgets/task/TaskView.vue → src/editor/widgets/task/TaskView.vue

0
src/designer/viewWidgets/toolbox/ToolboxMeta.ts → src/editor/widgets/toolbox/ToolboxMeta.ts

0
src/designer/viewWidgets/toolbox/ToolboxView.vue → src/editor/widgets/toolbox/ToolboxView.vue

61
src/model/example1.js → src/example/example1.js

@ -1,4 +1,5 @@
export default {
project_uuid: 'example1',
Tool: {
Group: [],
GlobalVariables: [],
@ -31,7 +32,7 @@ export default {
},
items: [
{
name: 'f1', t: 'floor', // 楼层
catalogCode: 'f1', t: 'floor', // 楼层
items: [
{
name: 'measure-group', t: 'measure', a: 'gp', // 类型, itemType.name == 'measure' 的组件处理. a:'gp' 代表分组, 渲染时他会是 Three.Group
@ -85,45 +86,45 @@ export default {
elevator: [],
wall: [],
pillar: [],
allLevels: [
catalog: [
{
value: 'F', label: '仓库楼层',
children: [
{ value: '-f1', label: '地下室 (-f1)' },
{ value: 'f1', label: '一楼 (f1)' },
{ value: 'f2', label: '二楼 (f2)' },
{ value: 'OUT', label: '外场 (OUT)' },
{ value: 'fe', label: '楼层电梯 (fe)' }
label: '仓库楼层',
items: [
{ catalogCode: '-f1', label: '地下室 (-f1)' },
{ catalogCode: 'f1', label: '一楼 (f1)' },
{ catalogCode: 'f2', label: '二楼 (f2)' },
{ catalogCode: 'OUT', label: '外场 (OUT)' },
{ catalogCode: '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)' }
label: '密集库区域',
items: [
{ catalogCode: 'm1', label: 'M1 (m1)' },
{ catalogCode: 'm2', label: 'M2 (m2)' },
{ catalogCode: 'm3', label: 'M3 (m3)' },
{ catalogCode: 'm4', label: 'M4 (m4)' },
{ catalogCode: 'me', label: '提升机 (me)' }
]
},
{
value: 'D', label: '多穿库A',
children: [
{ value: 'd1', label: 'D1 (d1)' },
{ value: 'd2', label: 'D2 (d2)' },
{ value: 'd3', label: 'D3 (d3)' },
{ value: 'd4', label: 'D4 (d4)' },
{ value: 'de1', label: '提升机 (de1)' }
label: '多穿库A',
items: [
{ catalogCode: 'd1', label: 'D1 (d1)' },
{ catalogCode: 'd2', label: 'D2 (d2)' },
{ catalogCode: 'd3', label: 'D3 (d3)' },
{ catalogCode: 'd4', label: 'D4 (d4)' },
{ catalogCode: 'de1', label: '提升机 (de1)' }
]
},
{
value: 'E', label: '多穿库B',
children: [
{ value: 'e1', label: 'E1 (e1)' },
{ value: 'e2', label: 'E2 (e2)' },
{ value: 'e3', label: 'E3 (e3)' },
{ value: 'e4', label: 'E4 (e4)' },
{ value: 'ee1', label: '提升机 (ee1)' }
label: '多穿库B',
items: [
{ catalogCode: 'e1', label: 'E1 (e1)' },
{ catalogCode: 'e2', label: 'E2 (e2)' },
{ catalogCode: 'e3', label: 'E3 (e3)' },
{ catalogCode: 'e4', label: 'E4 (e4)' },
{ catalogCode: 'ee1', label: '提升机 (ee1)' }
]
}
]

186
src/model/WorldModel.ts

@ -1,186 +0,0 @@
import _ from 'lodash'
import Example1 from './example1'
import { markRaw, reactive } from 'vue'
import * as THREE from 'three'
import { Scene } from 'three'
import type Viewport from '@/designer/Viewport.ts'
import { loadSceneFromJson } from '@/model/ModelUtils.ts'
import MeasureMeta from './itemType/measure/MeasureMeta'
import ConveyorMeta from './itemType/line/conveyor/ConveyorMeta'
import type { IGridHelper } from '@/model/WorldModelType.ts'
/**
*
*/
export default class WorldModel {
/**
*
*/
data: any = null
/**
*
*/
state = reactive({
openFileName: '',
allLevels: null,
})
sceneMap = new Map<string, Scene>()
viewPorts: Viewport[] = []
get gridOption(): IGridHelper {
const data = _.get(this.data, 'Tool.gridHelper')
return _.defaultsDeep(data, {
axesEnabled: true,
axesSize: 1000,
axesDivisions: 4,
axesColor: 0x000000,
axesOpacity: 1,
gridEnabled: true, // 启用网格
gridSize: 1000, // 网格大小, 单位米
gridDivisions: 1000, // 网格分割数
gridColor: 0x999999, // 网格颜色, 十六进制颜色值
gridOpacity: 0.8, // 网格透明度
snapEnabled: true, // 启用吸附
snapDistance: 0.25 // 吸附距离, 单位米
})
}
constructor() {
}
init() {
return Promise.all([
MeasureMeta.clazz.init(this),
ConveyorMeta.clazz.init(this)
]).then(() => {
console.log('世界模型初始化完成')
})
}
loadFloorToScene(viewport: Viewport, scene: THREE.Scene, levelCode: string) {
let floor = _.find(this.data.items, r => r.name === levelCode && r.t === 'floor')
if (!floor) {
console.info(`新建楼层: ${levelCode}`)
if (!_.isArray(this.data.items)) {
this.data.items = []
}
floor = { name: levelCode, t: 'floor', items: [] }
this.data.items.push(floor)
}
loadSceneFromJson(viewport, scene, floor.items)
}
open() {
if (this.sceneMap.size > 0) {
// 释放旧场景
this.sceneMap.forEach((scene: Scene) => {
this.sceneDispose(scene)
})
}
if (this.viewPorts.length > 0) {
// 注销视口
this.viewPorts.forEach((viewport: Viewport) => {
this.unregisterViewport(viewport)
})
}
system.msg('打开世界地图完成')
this.data = markRaw(Example1)
this.state.openFileName = 'example1'
this.state.allLevels = reactive(this.data.allLevels)
}
/**
* ,
*/
getSceneByFloor(viewport: Viewport, floor: string) {
if (this.sceneMap.has(floor)) {
return this.sceneMap.get(floor)
} else {
const scene = this.createScene(viewport, floor)
this.sceneMap.set(floor, scene)
return scene
}
}
/**
*
*/
createScene(viewport: Viewport, floor: string) {
const scene = new Scene()
scene.background = new THREE.Color(0xeeeeee)
this.loadFloorToScene(viewport, scene, floor)
return scene
}
/**
*
*/
registerViewport(viewport: Viewport) {
this.viewPorts = this.viewPorts || []
this.viewPorts.push(viewport)
}
/**
*
*/
unregisterViewport(viewport: Viewport) {
const index = this.viewPorts.indexOf(viewport)
if (index > -1) {
this.viewPorts.splice(index, 1)
}
}
/**
* , WebGL
*/
sceneDispose(scene: Scene = null) {
// 移除旧模型
if (!scene) {
return
}
scene.traverse((obj: any) => {
// 释放几何体
if (obj.geometry) {
obj.geometry.dispose()
}
// 释放材质
if (obj.material) {
if (Array.isArray(obj.material)) {
obj.material.forEach(m => m.dispose())
} else {
obj.material.dispose()
}
}
// 释放纹理
if (obj.texture) {
obj.texture.dispose()
}
// 释放渲染目标
if (obj.renderTarget) {
obj.renderTarget.dispose()
}
// 移除事件监听(如 OrbitControls)
if (obj.dispose) {
obj.dispose()
}
})
// 清空场景
scene.children = []
}
}

155
src/model/WorldModelType.ts

@ -1,155 +0,0 @@
import type { ActionType } from '@/model/itemType/ItemTypeDefine.ts'
export interface IGridHelper {
/**
*
*/
axesEnabled: boolean;
/**
* ,
*/
axesSize: number;
/**
*
*/
axesDivisions: number;
/**
* ,
*/
axesColor: number;
/**
*
*/
axesOpacity: number;
/**
*
*/
gridEnabled: boolean;
/**
* ,
*/
gridSize: number;
/**
*
*/
gridDivisions: number;
/**
* ,
*/
gridColor: number;
/**
*
*/
gridOpacity: number;
/**
*
*/
snapEnabled: boolean;
/**
* ,
*/
snapDistance: number;
}
/**
*
* :
* {
* id: 'p1', // 物体ID, 唯一标识, 需保证唯一, three.js 中的 uuid
* t: 'measure', // 物体类型, measure表示测量, 需交给 itemType.name == 'measure' 的组件处理
* a: 'ln', // 交互类型, ln表示线点操作, pt 表示点操作
* l: '测量1', // 标签名称, 显示用
* c: '#ff0000', // 颜色, 显示用. 十六进制颜色值, three.js 中的材质颜色
* tf: [ // 变换矩阵, 3x3矩阵, 采用Y轴向上为正, X轴向右, Z轴向前的右手坐标系
* [-9.0, 0, -1.0], // 平移向量 position
* [0, 0, 0], // 旋转向量 rotation, 表示绕Y轴旋转的角度, 单位为度。对应 three.js 应进行"角度"转"弧度"的换算
* [0.25, 0.1, 0.25] // 缩放向量 scale
* ],
* dt: { // 用户数据, 可自定义, 一般用在 three.js 的 userData 中
* link: ['p2'], // 用于 a='ln' 的测量线段, 关联的点对象(uuid)
* center: [], // 物流关联对象(uuid)
* in: [], // 物流入方向关联的对象(uuid)
* out: [] // 物流出方向关联的对象(uuid)
* }
* }
*/
export interface ItemJson {
/**
* , , three.js name , ,
*/
name?: string
/**
* three.js uuid, ID, ,
*/
id?: string
/**
* , defineItemType.name
*/
t: string
/**
*
*/
a: ActionType
/**
* , , three.js userData.label
*/
l?: string
/**
* , three.js userData.color
*/
c?: 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: {
/**
* (uuid)
*/
center?: string[]
/**
* (uuid)
*/
in?: string[]
/**
* (uuid)
*/
out?: string[]
/**
* ,
*/
[key: string]: any
},
/**
* , ,
*/
items: ItemJson[]
}

3
src/model/itemType/ItemTypeLine.ts

@ -99,6 +99,9 @@ export default abstract class ItemTypeLine extends ItemType {
}
}
/**
* ,
*/
createPoint(position: THREE.Vector3, item: ItemJson): THREE.Object3D {
const point = this.createPointBasic(position)
if (item.name) {

2
src/model/itemType/ToolboxLine.ts

@ -55,7 +55,7 @@ export default class ToolboxLine extends Toolbox {
if (this.viewport.state.selectedObject === point) {
// 如果当前选中的对象是要删除的点,则清除选中状态
this.viewport.state.selectedObject = undefined
EventBus.$emit('objectChanged', {
EventBus.dispatch('objectChanged', {
viewport: this,
object: null
})

5
src/modules/measure/MeasureEntity.ts

@ -0,0 +1,5 @@
import BaseEntity from '@/core/base/BaseItemEntity.ts'
export default class MeasureEntity extends BaseEntity {
}

18
src/modules/measure/MeasureInteraction.ts

@ -0,0 +1,18 @@
import * as THREE from 'three'
import type Viewport from '@/core/engine/Viewport.ts'
import BaseInteraction from '@/core/base/BaseInteraction.ts'
export default class MeasureInteraction extends BaseInteraction {
dragPointComplete(viewport: Viewport): void {
}
dragPointStart(viewport: Viewport, point: THREE.Object3D): void {
}
start(viewport: Viewport, startPoint?: THREE.Object3D): void {
}
stop(): void {
}
}

23
src/modules/measure/MeasureMeta.ts

@ -0,0 +1,23 @@
import type { IMeta } from '@/core/base/IMeta.ts'
const MeasureMeta: IMeta = {
// "点"属性面板
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: {}
}
export default MeasureMeta

40
src/modules/measure/MeasureRenderer.ts

@ -0,0 +1,40 @@
import type Viewport from '@/core/engine/Viewport.ts'
import BaseRenderer from '@/core/base/BaseRenderer.ts'
/**
*
*/
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: LinkType, option?: RendererCudOption) {
}
// 更新一根线
updateLine(start: ItemJson, end: ItemJson, type: LinkType, option?: RendererCudOption) {
}
// 删除一根线
deleteLine(start: ItemJson, end: ItemJson, type: LinkType, option?: RendererCudOption) {
}
// 结束更新
endUpdate(viewport: Viewport) {
}
}

13
src/modules/measure/index.ts

@ -0,0 +1,13 @@
import { defineModule } from '@/core/manager/ModuleManager.ts'
import MeasureRenderer from './MeasureRenderer.ts'
import MeasureEntity from './MeasureEntity.ts'
import MeasureMeta from './MeasureMeta.ts'
import MeasureInteraction from './MeasureInteraction.ts'
defineModule({
name: 'measure',
renderer: new MeasureRenderer(),
interaction: new MeasureInteraction(),
meta: MeasureMeta,
entity: MeasureEntity
})

2
src/router/index.ts

@ -1,6 +1,6 @@
import { createRouter, createWebHashHistory } from 'vue-router'
// import HomeView from '../views/HomeView.vue'
import ModelMain from '../views/ModelMain.vue'
import ModelMain from '../editor/ModelMain.vue'
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),

9
src/runtime/EventBus.js

@ -1,9 +0,0 @@
import mitt from 'mitt'
const instance = mitt()
export default {
$emit: instance.emit,
$on: instance.on,
$off: instance.off
}

17
src/runtime/EventBus.ts

@ -0,0 +1,17 @@
import mitt from 'mitt'
const instance = mitt()
export type DispatchNames = 'objectChanged' | 'catalogChanged'
export default {
dispatch(name: DispatchNames, data?: any) {
instance.emit(name, data)
},
on(name: DispatchNames, callback: (data?: any) => void) {
instance.on(name, callback)
},
off(name: DispatchNames, callback: (data?: any) => void) {
instance.off(name, callback)
}
}

4
src/runtime/System.ts

@ -6,7 +6,7 @@ import hotkeys from 'hotkeys-js'
import { defineComponent, h, markRaw, nextTick, reactive, toRaw, unref, type App, createApp, type Component } from 'vue'
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import { QuestionFilled } from '@element-plus/icons-vue'
import { decompressUUID, renderIcon, createShortUUID } from '@/utils/webutils.ts'
import { decompressUUID, renderIcon, createShortUUID, setQueryParam, getQueryParams } from '@/utils/webutils.ts'
import type { showDialogOption } from '@/SystemOption'
import ShowDialogWrap from '@/components/ShowDialogWrap.vue'
import LoadingDialog from '@/components/LoadingDialog.vue'
@ -40,6 +40,8 @@ export default class System {
createUUID = createShortUUID
decompressUUID = decompressUUID
setQueryParam = setQueryParam
getQueryParams = getQueryParams
constructor(app: App) {
this.app = app

50
src/types/Types.d.ts

@ -1,4 +1,4 @@
export type CursorMode =
type CursorMode =
'normal'
| 'ALink'
| 'SLink'
@ -11,16 +11,7 @@ export type CursorMode =
| 'MeasureAngle'
| 'selectByRec'
type PointType =
'measure-marker'
| 'pointMarker'
| 'pointMarker2'
| 'pointMarker3'
| 'pointMarker4'
| 'pointMarker5'
export export interface VData {
interface VData {
/**
*
*/
@ -30,21 +21,47 @@ export export interface VData {
*
*/
isChanged: boolean
/**
* id
*/
id: string
/**
*
*/
catalog: Catalog
}
interface CatalogItem {
catalogCode: string; // 楼层代码
label: string; // 楼层显示名称
}
interface CatalogGroup {
label: string; // 楼层组名称
items: CatalogItem[]; // 楼层组中的楼层列表
}
export interface VDataItem {
/**
*
*/
type Catalog = CatalogGroup[];
interface VDataItem {
/**
* {
* id: 'p1', // 物体ID, 唯一标识, 需保证唯一, three.js 中的 uuid
* t: 'measure', // 物体类型, measure表示测量, 需交给 itemType.name == 'measure' 的组件处理
* l: '测量1', // 标签名称, 显示用
* c: '#ff0000', // 颜色, 显示用. 十六进制颜色值, three.js 中的材质颜色
* name: '', // 物体ID, 显示在 Tree 节点用
* tf: [ // 变换矩阵, 3x3矩阵, 采用Y轴向上为正, X轴向右, Z轴向前的右手坐标系
* [-9.0, 0, -1.0], // 平移向量 position
* [0, 0, 0], // 旋转向量 rotation, 表示绕Y轴旋转的角度, 单位为度。对应 three.js 应进行"角度"转"弧度"的换算
* [0.25, 0.1, 0.25] // 缩放向量 scale
* ],
* dt: { // 用户数据, 可自定义, 一般用在 three.js 的 userData 中
* label: '测量1', // 标签名称, 显示在 Tree/Map 用
* color: '#ff0000', // 颜色, 显示用. 十六进制颜色值, three.js 中的材质颜色
* center: ['p2'], // 用于 a='ln' 的测量线段, 关联的点对象(uuid)
* in: [], // 物流入方向关联的对象(uuid)
* out: [] // 物流出方向关联的对象(uuid)
@ -54,14 +71,15 @@ export interface VDataItem {
*/
id: string
t: string
l: string
c: string
name: string
tf: [
[number, number, number],
[number, number, number],
[number, number, number],
]
dt: {
label: string
color: string
center?: string[] // 用于 a='ln' 的测量线段, 关联的点对象(uuid)
in?: string[] // 物流入方向关联的对象(uuid)
out?: string[] // 物流出方向关联的对象(uuid)

2
src/types/global.d.ts

@ -1,7 +1,7 @@
import _ from 'lodash'
import $ from 'jquery'
import type System from '@/runtime/System'
import type WorldModel from '@/model/WorldModel'
import type WorldModel from '@/core/manager/WorldModel'
declare global {
const $: $

181
src/types/model.d.ts

@ -0,0 +1,181 @@
type LinkType = 'in' | 'out' | 'center'
interface InitThreeOption {
stateManagerId: string
}
interface InteractionCudOption {
}
/**
*
*/
interface RendererCudOption {
// Add any additional options needed for create, update, delete operations
}
/**
*
*/
interface EntityCudOption {
// Additional options for create, update, delete operations
}
interface IGridHelper {
/**
*
*/
axesEnabled: boolean;
/**
* ,
*/
axesSize: number;
/**
*
*/
axesDivisions: number;
/**
* ,
*/
axesColor: number;
/**
*
*/
axesOpacity: number;
/**
*
*/
gridEnabled: boolean;
/**
* ,
*/
gridSize: number;
/**
*
*/
gridDivisions: number;
/**
* ,
*/
gridColor: number;
/**
*
*/
gridOpacity: number;
/**
*
*/
snapEnabled: boolean;
/**
* ,
*/
snapDistance: number;
}
/**
* ()
* :
* {
* id: 'p1', // 物体唯一ID, 也用于 three.js 中的 uuid
* t: 'measure', // 物体类型, measure表示测量, 需交给 itemType.name == 'measure' 的组件处理
* tf: [ // 变换矩阵, 3x3矩阵, 采用Y轴向上为正, X轴向右, Z轴向前的右手坐标系
* [-9.0, 0, -1.0], // 平移向量 position
* [0, 0, 0], // 旋转向量 rotation, 表示绕Y轴旋转的角度, 单位为度。对应 three.js 应进行"角度"转"弧度"的换算
* [0.25, 0.1, 0.25] // 缩放向量 scale
* ],
* dt: { // 用户数据, 可自定义, 一般用在 three.js 的 userData 中
* label: '测量1', // 标签名称, 显示用
* color: '#ff0000', // 颜色, 显示用. 十六进制颜色值, three.js 中的材质颜色
* center: ['p2'], // S连线(又称逻辑连线), 与其他点之间的无方向性关联, 关系的起点需要在他的 dt.center[] 数组中添加目标点的id, 关系的终点需要在他的 dt.center[] 数组中添加起点的 id
* in: [], // A连线(又称物体流动线)的输入, 关系的终点需要在 dt.in[] 数组中添加起点的 id
* out: [] // A连线(又称物体流动线)的输出, 关系的起点需要在 dt.out[] 数组中添加目标点的 id
* ...
* }
* }
*/
interface ItemJson {
/**
* three.js uuid, ID, , , O(1)
*/
id?: string
/**
* , , three.js name , , , O(N)
*/
name?: string
/**
* "点", measure / conveyor / task
*/
t: string
/**
* , THREE.Object3D visible
*/
v: boolean
/**
* , 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
},
}

33
src/utils/webutils.ts

@ -6,6 +6,39 @@ import * as FaIcon from '@vicons/fa'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import * as THREE from 'three'
export function getQueryParams() {
// const search = window.location.search || window.location.hash.split('?')[1] || ''
// const params = new URLSearchParams(search)
// return params.get(name) || null
const hash = window.location.hash || '#'
const match = hash.split('?')[1] || ''
return new URLSearchParams(match)
}
export function setQueryParam(key: string, value: string) {
// const url = new URL(window.location.href)
// if (value) {
// // 如果 value 为空,则删除该参数
// url.searchParams.set(key, value)
// } else {
// url.searchParams.delete(key)
// }
// const newUrl = url.toString()
// window.history.pushState({}, '', newUrl)
let [base, queryStr] = window.location.hash.split('?')
const params = new URLSearchParams(queryStr || '')
if (!value) {
params.delete(key) // 删除参数
} else {
params.set(key, value) // 设置或更新参数
}
const newQuery = params.toString()
const newHash = base + (newQuery ? '?' + newQuery : '')
window.location.hash = newHash
}
/**
* 0x409EFF '#409EFF'
*/

Loading…
Cancel
Save