121 changed files with 7921 additions and 6231 deletions
@ -1,19 +1,70 @@ |
|||||
# yvan-rcs-web |
# 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 * as THREE from 'three' |
||||
import type { ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine.ts' |
import type { ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine.ts' |
||||
import type { ItemJson } from '@/model/WorldModelType.ts' |
|
||||
import { getAllItemTypes, getItemTypeByName } from '@/model/itemType/ItemTypeDefine.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 { computeBoundsTree, disposeBoundsTree } from 'three-mesh-bvh' |
||||
import { Vector2 } from 'three/src/math/Vector2' |
import { Vector2 } from 'three/src/math/Vector2' |
||||
import type Toolbox from '@/model/itemType/Toolbox.ts' |
import type Toolbox from '@/model/itemType/Toolbox.ts' |
||||
|
|
||||
export function deletePointByKeyboard() { |
export function deletePointByKeyboard() { |
||||
const viewport: Viewport = window['viewport'] |
system.msg('Delete not impleted yet') |
||||
if (!viewport) { |
// const viewport: Viewport = window['viewport']
|
||||
system.msg('没有找到当前视图') |
// if (!viewport) {
|
||||
return |
// system.msg('没有找到当前视图')
|
||||
} |
// return
|
||||
|
// }
|
||||
// 按下 Delete 键,删除当前选中的点
|
//
|
||||
if (!viewport.state.selectedObject) { |
// // 按下 Delete 键,删除当前选中的点
|
||||
system.msg('没有选中任何点') |
// if (!viewport.state.selectedObject) {
|
||||
return |
// system.msg('没有选中任何点')
|
||||
} |
// return
|
||||
|
// }
|
||||
const selectedObject = viewport.state.selectedObject |
//
|
||||
if (!(selectedObject instanceof THREE.Object3D)) { |
// const selectedObject = viewport.state.selectedObject
|
||||
system.msg('选中的对象不是有效的点') |
// if (!(selectedObject instanceof THREE.Object3D)) {
|
||||
return |
// system.msg('选中的对象不是有效的点')
|
||||
} |
// return
|
||||
|
// }
|
||||
if (!selectedObject.userData?.type) { |
//
|
||||
system.msg('选中的对象没有类型信息') |
// if (!selectedObject.userData?.type) {
|
||||
return |
// system.msg('选中的对象没有类型信息')
|
||||
} |
// return
|
||||
|
// }
|
||||
const toolbox: Toolbox = viewport.toolbox[selectedObject.userData.type] |
//
|
||||
if (!toolbox) { |
// const toolbox: Toolbox = viewport.toolbox[selectedObject.userData.type]
|
||||
system.msg('没有找到对应的工具箱') |
// if (!toolbox) {
|
||||
return |
// system.msg('没有找到对应的工具箱')
|
||||
} |
// return
|
||||
|
// }
|
||||
viewport.state.cursorMode = 'normal' |
//
|
||||
toolbox.deletePoint(selectedObject) |
// viewport.state.cursorMode = 'normal'
|
||||
|
// toolbox.deletePoint(selectedObject)
|
||||
} |
} |
||||
|
|
||||
export function escByKeyboard() { |
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 |
init(viewport: any): void |
||||
|
|
||||
destory(): 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 { renderIcon } from '@/utils/webutils.ts' |
||||
import { defineMenu } from '@/runtime/DefineMenu.ts' |
import { defineMenu } from '@/runtime/DefineMenu.ts' |
||||
import SvgCode from '@/components/icons/SvgCode' |
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) => { |
export default defineMenu((menus) => { |
||||
menus.insertChildren('modelFile', |
menus.insertChildren('modelFile', |
||||
@ -1,5 +1,5 @@ |
|||||
import { defineMenu } from '@/runtime/DefineMenu.ts' |
import { defineMenu } from '@/runtime/DefineMenu.ts' |
||||
import Model3DView from '@/designer/model3DView/Model3DView.vue' |
import Model3DView from '@/components/Model3DView.vue' |
||||
|
|
||||
export default defineMenu((menus) => { |
export default defineMenu((menus) => { |
||||
menus.insertChildren('tool', |
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