121 changed files with 7921 additions and 6231 deletions
@ -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 # 查看器页面 |
|||
``` |
|||
@ -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 个以上的点, 是否存在性能问题? |
|||
|
|||
这种封装是否合理, 有什么优化建议? |
|||
|
After Width: | Height: | Size: 214 KiB |
@ -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() { |
|||
@ -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 |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -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
|
|||
} |
|||
} |
|||
|
|||
@ -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; |
|||
} |
|||
@ -1,4 +1,4 @@ |
|||
export interface ITool { |
|||
export default interface IControls { |
|||
init(viewport: any): void |
|||
|
|||
destory(): void |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -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) |
|||
} |
|||
} |
|||
@ -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); |
|||
` |
|||
) |
|||
} |
|||
} |
|||
@ -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() { |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
@ -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)
|
|||
// }
|
|||
// }
|
|||
|
|||
} |
|||
@ -1,8 +0,0 @@ |
|||
<template> |
|||
<div class="section-canvas"> |
|||
<div class="section-content"></div> |
|||
</div> |
|||
</template> |
|||
<script> |
|||
export default {} |
|||
</script> |
|||
@ -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> |
|||
@ -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 |
|||
} |
|||
} |
|||
}) |
|||
@ -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> |
|||
@ -0,0 +1,8 @@ |
|||
<template> |
|||
Model3DViewer not implemented yet. |
|||
</template> |
|||
<script> |
|||
export default { |
|||
|
|||
} |
|||
</script> |
|||
@ -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', |
|||
@ -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', |
|||
@ -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 = [] |
|||
} |
|||
} |
|||
@ -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[] |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
import BaseEntity from '@/core/base/BaseItemEntity.ts' |
|||
|
|||
export default class MeasureEntity extends BaseEntity { |
|||
|
|||
} |
|||
@ -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 { |
|||
} |
|||
|
|||
} |
|||
@ -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 |
|||
@ -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) { |
|||
} |
|||
} |
|||
@ -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 |
|||
}) |
|||
@ -1,9 +0,0 @@ |
|||
import mitt from 'mitt' |
|||
|
|||
const instance = mitt() |
|||
|
|||
export default { |
|||
$emit: instance.emit, |
|||
$on: instance.on, |
|||
$off: instance.off |
|||
} |
|||
@ -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) |
|||
} |
|||
} |
|||
@ -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 |
|||
}, |
|||
} |
|||
Loading…
Reference in new issue