From b4bbcff248d2beb433bf04bae57a8cd01be03867 Mon Sep 17 00:00:00 2001 From: yvan Date: Sun, 1 Jun 2025 03:09:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=A7=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 83 +- doc/物流模型总体介绍.md | 658 +++++++++++++++ src/assets/images/conveyor/shapes/logo.png | Bin 0 -> 219515 bytes src/components/Model3DView.vue | 921 +++++++++++++++++++++ src/core/Constract.ts | 20 + src/core/ModelUtils.ts | 292 +++++++ src/core/base/BaseInteraction.ts | 35 + src/core/base/BaseItemEntity.ts | 26 + src/core/base/BaseRenderer.ts | 85 ++ src/core/base/IMeta.ts | 55 ++ src/core/controls/DragControls.js | 209 +++++ src/core/controls/EsDragControls.ts | 155 ++++ src/core/controls/IControls.ts | 7 + src/core/controls/MouseMoveInspect.ts | 76 ++ src/core/controls/SelectInspect.ts | 213 +++++ src/core/engine/SceneHelp.ts | 133 +++ src/core/engine/Viewport.ts | 483 +++++++++++ src/core/manager/EntityManager.ts | 157 ++++ src/core/manager/InstancePool.ts | 151 ++++ src/core/manager/InteractionManager.ts | 50 ++ src/core/manager/ModuleManager.ts | 82 ++ src/core/manager/StateManager.ts | 562 +++++++++++++ src/core/manager/WorldModel.ts | 180 ++++ src/designer/Constract.ts | 20 - src/designer/ModelView.vue | 8 - src/designer/StateManager.ts | 554 ------------- src/designer/Viewport.ts | 540 ------------ src/designer/menus/EditMenu.ts | 111 --- src/designer/menus/FileMenu.ts | 32 - src/designer/menus/Model3DView.ts | 27 - src/designer/menus/Tools.ts | 11 - src/designer/metaComponents/ColorItem.vue | 34 - src/designer/metaComponents/IMetaProp.ts | 34 - src/designer/metaComponents/NumberInput.vue | 33 - src/designer/metaComponents/SwitchItem.vue | 33 - src/designer/metaComponents/TextInput.vue | 33 - src/designer/metaComponents/Transform.vue | 170 ---- src/designer/metaComponents/UUIDItem.vue | 33 - src/designer/model2DEditor/DragControls.js | 209 ----- src/designer/model2DEditor/EsDragControls.ts | 156 ---- src/designer/model2DEditor/Model2DEditor.vue | 73 -- src/designer/model2DEditor/Model2DEditorJs.js | 91 -- src/designer/model2DEditor/tools/ITool.ts | 7 - .../model2DEditor/tools/MouseMoveInspect.ts | 76 -- src/designer/model2DEditor/tools/SelectInspect.ts | 213 ----- src/designer/model3DView/Model3DView.vue | 921 --------------------- src/designer/viewWidgets/IWidgets.ts | 40 - src/designer/viewWidgets/alarm/AlarmMeta.ts | 12 - src/designer/viewWidgets/alarm/AlarmView.vue | 168 ---- src/designer/viewWidgets/logger/LoggerMeta.ts | 12 - src/designer/viewWidgets/logger/LoggerView.vue | 69 -- .../viewWidgets/modeltree/ModeltreeMeta.ts | 13 - .../viewWidgets/modeltree/ModeltreeView.vue | 28 - .../viewWidgets/modeltree/ModeltreeViewJs.js | 99 --- src/designer/viewWidgets/monitor/MonitorMeta.ts | 12 - src/designer/viewWidgets/monitor/MonitorView.vue | 243 ------ src/designer/viewWidgets/property/PropertyMeta.ts | 13 - src/designer/viewWidgets/property/PropertyView.vue | 143 ---- src/designer/viewWidgets/script/ScriptMeta.ts | 12 - src/designer/viewWidgets/script/ScriptView.vue | 29 - src/designer/viewWidgets/task/TaskMeta.ts | 12 - src/designer/viewWidgets/task/TaskView.vue | 183 ---- src/designer/viewWidgets/toolbox/ToolboxMeta.ts | 12 - src/designer/viewWidgets/toolbox/ToolboxView.vue | 173 ---- src/editor/Model2DEditor.vue | 213 +++++ src/editor/Model3DViewer.vue | 8 + src/editor/ModelMain.less | 355 ++++++++ src/editor/ModelMain.vue | 307 +++++++ src/editor/ModelMainInit.ts | 65 ++ src/editor/menus/EditMenu.ts | 111 +++ src/editor/menus/FileMenu.ts | 37 + src/editor/menus/Model3DView.ts | 27 + src/editor/menus/Tools.ts | 11 + src/editor/propEditors/ColorItem.vue | 34 + src/editor/propEditors/IMetaProp.ts | 34 + src/editor/propEditors/NumberInput.vue | 33 + src/editor/propEditors/SwitchItem.vue | 33 + src/editor/propEditors/TextInput.vue | 33 + src/editor/propEditors/Transform.vue | 170 ++++ src/editor/propEditors/UUIDItem.vue | 33 + src/editor/widgets/IWidgets.ts | 40 + src/editor/widgets/alarm/AlarmMeta.ts | 12 + src/editor/widgets/alarm/AlarmView.vue | 168 ++++ src/editor/widgets/logger/LoggerMeta.ts | 12 + src/editor/widgets/logger/LoggerView.vue | 69 ++ src/editor/widgets/modeltree/ModeltreeMeta.ts | 13 + src/editor/widgets/modeltree/ModeltreeView.vue | 30 + src/editor/widgets/modeltree/ModeltreeViewJs.js | 116 +++ src/editor/widgets/monitor/MonitorMeta.ts | 12 + src/editor/widgets/monitor/MonitorView.vue | 243 ++++++ src/editor/widgets/property/PropertyMeta.ts | 13 + src/editor/widgets/property/PropertyView.vue | 143 ++++ src/editor/widgets/script/ScriptMeta.ts | 12 + src/editor/widgets/script/ScriptView.vue | 29 + src/editor/widgets/task/TaskMeta.ts | 12 + src/editor/widgets/task/TaskView.vue | 183 ++++ src/editor/widgets/toolbox/ToolboxMeta.ts | 12 + src/editor/widgets/toolbox/ToolboxView.vue | 173 ++++ src/example/example1.js | 131 +++ src/model/ModelUtils.ts | 292 ------- src/model/WorldModel.ts | 186 ----- src/model/WorldModelType.ts | 155 ---- src/model/example1.js | 130 --- src/model/itemType/ItemTypeLine.ts | 3 + src/model/itemType/ToolboxLine.ts | 2 +- src/modules/measure/MeasureEntity.ts | 5 + src/modules/measure/MeasureInteraction.ts | 18 + src/modules/measure/MeasureMeta.ts | 23 + src/modules/measure/MeasureRenderer.ts | 40 + src/modules/measure/index.ts | 13 + src/router/index.ts | 2 +- src/runtime/EventBus.js | 9 - src/runtime/EventBus.ts | 17 + src/runtime/System.ts | 4 +- src/types/Types.d.ts | 58 +- src/types/global.d.ts | 2 +- src/types/model.d.ts | 181 ++++ src/utils/webutils.ts | 33 + src/views/ModelMain.less | 355 -------- src/views/ModelMain.vue | 305 ------- src/views/ModelMainInit.ts | 67 -- 121 files changed, 7921 insertions(+), 6231 deletions(-) create mode 100644 doc/物流模型总体介绍.md create mode 100644 src/assets/images/conveyor/shapes/logo.png create mode 100644 src/components/Model3DView.vue create mode 100644 src/core/Constract.ts create mode 100644 src/core/ModelUtils.ts create mode 100644 src/core/base/BaseInteraction.ts create mode 100644 src/core/base/BaseItemEntity.ts create mode 100644 src/core/base/BaseRenderer.ts create mode 100644 src/core/base/IMeta.ts create mode 100644 src/core/controls/DragControls.js create mode 100644 src/core/controls/EsDragControls.ts create mode 100644 src/core/controls/IControls.ts create mode 100644 src/core/controls/MouseMoveInspect.ts create mode 100644 src/core/controls/SelectInspect.ts create mode 100644 src/core/engine/SceneHelp.ts create mode 100644 src/core/engine/Viewport.ts create mode 100644 src/core/manager/EntityManager.ts create mode 100644 src/core/manager/InstancePool.ts create mode 100644 src/core/manager/InteractionManager.ts create mode 100644 src/core/manager/ModuleManager.ts create mode 100644 src/core/manager/StateManager.ts create mode 100644 src/core/manager/WorldModel.ts delete mode 100644 src/designer/Constract.ts delete mode 100644 src/designer/ModelView.vue delete mode 100644 src/designer/StateManager.ts delete mode 100644 src/designer/Viewport.ts delete mode 100644 src/designer/menus/EditMenu.ts delete mode 100644 src/designer/menus/FileMenu.ts delete mode 100644 src/designer/menus/Model3DView.ts delete mode 100644 src/designer/menus/Tools.ts delete mode 100644 src/designer/metaComponents/ColorItem.vue delete mode 100644 src/designer/metaComponents/IMetaProp.ts delete mode 100644 src/designer/metaComponents/NumberInput.vue delete mode 100644 src/designer/metaComponents/SwitchItem.vue delete mode 100644 src/designer/metaComponents/TextInput.vue delete mode 100644 src/designer/metaComponents/Transform.vue delete mode 100644 src/designer/metaComponents/UUIDItem.vue delete mode 100644 src/designer/model2DEditor/DragControls.js delete mode 100644 src/designer/model2DEditor/EsDragControls.ts delete mode 100644 src/designer/model2DEditor/Model2DEditor.vue delete mode 100644 src/designer/model2DEditor/Model2DEditorJs.js delete mode 100644 src/designer/model2DEditor/tools/ITool.ts delete mode 100644 src/designer/model2DEditor/tools/MouseMoveInspect.ts delete mode 100644 src/designer/model2DEditor/tools/SelectInspect.ts delete mode 100644 src/designer/model3DView/Model3DView.vue delete mode 100644 src/designer/viewWidgets/IWidgets.ts delete mode 100644 src/designer/viewWidgets/alarm/AlarmMeta.ts delete mode 100644 src/designer/viewWidgets/alarm/AlarmView.vue delete mode 100644 src/designer/viewWidgets/logger/LoggerMeta.ts delete mode 100644 src/designer/viewWidgets/logger/LoggerView.vue delete mode 100644 src/designer/viewWidgets/modeltree/ModeltreeMeta.ts delete mode 100644 src/designer/viewWidgets/modeltree/ModeltreeView.vue delete mode 100644 src/designer/viewWidgets/modeltree/ModeltreeViewJs.js delete mode 100644 src/designer/viewWidgets/monitor/MonitorMeta.ts delete mode 100644 src/designer/viewWidgets/monitor/MonitorView.vue delete mode 100644 src/designer/viewWidgets/property/PropertyMeta.ts delete mode 100644 src/designer/viewWidgets/property/PropertyView.vue delete mode 100644 src/designer/viewWidgets/script/ScriptMeta.ts delete mode 100644 src/designer/viewWidgets/script/ScriptView.vue delete mode 100644 src/designer/viewWidgets/task/TaskMeta.ts delete mode 100644 src/designer/viewWidgets/task/TaskView.vue delete mode 100644 src/designer/viewWidgets/toolbox/ToolboxMeta.ts delete mode 100644 src/designer/viewWidgets/toolbox/ToolboxView.vue create mode 100644 src/editor/Model2DEditor.vue create mode 100644 src/editor/Model3DViewer.vue create mode 100644 src/editor/ModelMain.less create mode 100644 src/editor/ModelMain.vue create mode 100644 src/editor/ModelMainInit.ts create mode 100644 src/editor/menus/EditMenu.ts create mode 100644 src/editor/menus/FileMenu.ts create mode 100644 src/editor/menus/Model3DView.ts create mode 100644 src/editor/menus/Tools.ts create mode 100644 src/editor/propEditors/ColorItem.vue create mode 100644 src/editor/propEditors/IMetaProp.ts create mode 100644 src/editor/propEditors/NumberInput.vue create mode 100644 src/editor/propEditors/SwitchItem.vue create mode 100644 src/editor/propEditors/TextInput.vue create mode 100644 src/editor/propEditors/Transform.vue create mode 100644 src/editor/propEditors/UUIDItem.vue create mode 100644 src/editor/widgets/IWidgets.ts create mode 100644 src/editor/widgets/alarm/AlarmMeta.ts create mode 100644 src/editor/widgets/alarm/AlarmView.vue create mode 100644 src/editor/widgets/logger/LoggerMeta.ts create mode 100644 src/editor/widgets/logger/LoggerView.vue create mode 100644 src/editor/widgets/modeltree/ModeltreeMeta.ts create mode 100644 src/editor/widgets/modeltree/ModeltreeView.vue create mode 100644 src/editor/widgets/modeltree/ModeltreeViewJs.js create mode 100644 src/editor/widgets/monitor/MonitorMeta.ts create mode 100644 src/editor/widgets/monitor/MonitorView.vue create mode 100644 src/editor/widgets/property/PropertyMeta.ts create mode 100644 src/editor/widgets/property/PropertyView.vue create mode 100644 src/editor/widgets/script/ScriptMeta.ts create mode 100644 src/editor/widgets/script/ScriptView.vue create mode 100644 src/editor/widgets/task/TaskMeta.ts create mode 100644 src/editor/widgets/task/TaskView.vue create mode 100644 src/editor/widgets/toolbox/ToolboxMeta.ts create mode 100644 src/editor/widgets/toolbox/ToolboxView.vue create mode 100644 src/example/example1.js delete mode 100644 src/model/ModelUtils.ts delete mode 100644 src/model/WorldModel.ts delete mode 100644 src/model/WorldModelType.ts delete mode 100644 src/model/example1.js create mode 100644 src/modules/measure/MeasureEntity.ts create mode 100644 src/modules/measure/MeasureInteraction.ts create mode 100644 src/modules/measure/MeasureMeta.ts create mode 100644 src/modules/measure/MeasureRenderer.ts create mode 100644 src/modules/measure/index.ts delete mode 100644 src/runtime/EventBus.js create mode 100644 src/runtime/EventBus.ts create mode 100644 src/types/model.d.ts delete mode 100644 src/views/ModelMain.less delete mode 100644 src/views/ModelMain.vue delete mode 100644 src/views/ModelMainInit.ts diff --git a/README.md b/README.md index dad8473..d509ca4 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,70 @@ # yvan-rcs-web -## Project Setup - -```sh -yarn -``` - -### Compile and Hot-Reload for Development - -```sh -yarn dev -``` - -### Type-Check, Compile and Minify for Production - -```sh -yarn build +## 文件结构构成 ``` +src/ +├── assets/ # 静态资源(纹理、图标等) +├── components/ # 一些公共组件 +├── core/ # 核心类库(不依赖 Vue,便于复用) +│ ├── example/ # 各种实体基础类 +│ │ └── Example1.js +│ ├── base/ # 各种实体基础类 +│ │ ├── BaseRenderer.ts +│ │ ├── BaseInteraction.ts +│ │ ├── BaseMeta.ts +│ │ ├── BaseItemEntity.ts +│ │ └── BaseLineEntity.ts +│ ├── manager/ # 管理器类 +│ │ ├── ModuleManager.ts +│ │ ├── StateManager.ts +│ │ ├── WorldModel.ts +│ │ ├── EntityManager.ts +│ │ └── InstancePool.ts +│ ├── utils/ # 管理器类 +│ │ ├── StateManager.ts +│ │ └── WorldModel.ts +│ └── engine/ # Three.js 封装类 +│ ├── SceneHelp.ts +│ └── Viewport.ts +├── editor/ # 编辑器 +│ ├── menus/ # 各种实体基础类 +│ │ ├── FileMenu.ts +│ │ ├── EditMenu.ts +│ │ ├── Model3DView.ts +│ │ └── Tools.ts +│ ├── widgets/ # 管理器类 +│ │ └── ... +│ ├── propEditors/ # 属性面板编辑器 +│ │ └── ... +│ ├── controls/ # 各种实体基础类 +│ │ ├── SelectionControls.ts +│ │ ├── EsDragControls.ts +│ │ └── MouseMoveControls.ts +│ ├── Model3DViewer.vue +│ ├── Model2DEditor.vue +│ └── EditorMain.vue # Three.js 封装类 +├── modules/ # 模块化插件目录(按物流单元类型组织) +│ ├── measure/ # 测量单元模块 +│ │ ├── MeasureRenderer.ts +│ │ ├── MeasureInteraction.ts +│ │ ├── MeasureMeta.ts +│ │ ├── MeasureEntity.ts +│ │ └── index.ts +│ ├── conveyor/ # 输送线模块 +│ │ ├── ConveyorRenderer.ts +│ │ ├── ConveyorInteraction.ts +│ │ ├── ConveyorMeta.ts +│ │ ├── ConveyorEntity.ts +│ │ └── index.ts +│ └── ... # 其他物流单元模块 +├── plugins/ # 插件系统支持 +│ └── registerItemType.ts # 注册物流单元类型的插件机制 +├── types/ # 类型定义(全局共享的类型) +│ ├── model.d.ts +│ └── index.d.ts +├── utils/ # 工具函数(非 Three 相关) +│ └── index.ts +└── views/ # 页面视图(Vue 页面) + ├── Editor.vue # 主编辑器页面 + └── Viewer.vue # 查看器页面 +``` \ No newline at end of file diff --git a/doc/物流模型总体介绍.md b/doc/物流模型总体介绍.md new file mode 100644 index 0000000..31f8fcd --- /dev/null +++ b/doc/物流模型总体介绍.md @@ -0,0 +1,658 @@ +# 物流模型总体介绍 +## 基本定义 + +### 物流单元大纲 +- 点 point + + - 辅助定位点 point + - 决策点 decision\_point + - 扫码器 bcr + - 站点 station\_point +- 线 line + + - 输送线 conveyor + - 行走路径 moveline + - 辅助测量线 measure + + - 弧线类型 + + - 直线 line + - 贝塞尔曲线 bessel + - 圆弧线 curved +- 存储 store + + - 暂存区 queue + - 地堆区 ground\_rack + - 常规货架 rack + - 立库货架 asrs\_rack + - 密集库货架 flash\_rack + - 多穿库货架 shuttle\_rack + - 层间线 pd +- 任务执行器 executer + + - 堆垛机 stacker + - 两向穿梭车 laser + - 四向穿梭车 flash + - 穿梭板 flash\_tp + - 货物提升机 life + - 车提升机 flash\_life + - 叉车 forklift + - 侧叉式AGV ptr + - 潜伏式AGV agv + - 背篓式AGV CTU + - 人工 people + - 机械手 robotic\_arm + - 碟盘机 stacking + - 装卸塔 dump_tower + - 加工台 station + - 电子标签 tag +- 流动单元 flow\_item + + - box 纸箱 + - tote 周转箱 + - pallet 托盘 +- 辅助 other + + - 发生器 source + - 消失器 sink + - 任务分配器 dispatcher + - 文本 text + - 图片 image + - 区域 plane + + + +### 物流世界 + +一个物流仓库, 就是一个世界 + +他有自己的项目定义, 楼层, 围墙, 柱子, 其他数据, 个性化脚本等等 + +每次对建模文件打开的时候, 因为性能问题, 一次只会读取一个楼层, 或者一个水平横截面. + +因此, 一个 floor 就是对应一个 THREE.Scene + +```ts +/** + * 物流世界模型 + */ +export default class WorldModel { + /** + * 所有楼层 / 提升机横截面的目录 + */ + allLevels = [ + { + value: 'F', label: '仓库楼层', + children: [ + { value: '-f1', label: '地下室 (-f1)' }, + { value: 'f1', label: '一楼 (f1)' }, + { value: 'f2', label: '二楼 (f2)' }, + { value: 'OUT', label: '外场 (OUT)' }, + { value: 'fe', label: '楼层电梯 (fe)' } + ] + }, + { + value: 'M', label: '密集库区域', + children: [ + { value: 'm1', label: 'M1 (m1)' }, + { value: 'm2', label: 'M2 (m2)' }, + { value: 'm3', label: 'M3 (m3)' }, + { value: 'm4', label: 'M4 (m4)' }, + { value: 'me', label: '提升机 (me)' } + ] + }, + ] + + // 载入某个楼层 + async loadFloor(floorId: string): Promise +} +``` + + +物流控制中心系统,基于 ThreeJS 和 Vue3 开发. + +他的主要作用就是通过Web浏览器, 建立一个自动化立体仓库的物流模型 + + +### 单元点 ItemJson + +```ts +/** + * 物体单元(点) + */ +export interface ItemJson { + /** + * 对应 three.js 中的 uuid, 物体ID, 唯一标识, 需保证唯一, 有方法可以进行快速的 O(1) 查找 + */ + id?: string + + /** + * 物体名称, 显示用, 最后初始化到 three.js 的 name 中, 可以不设置, 可以不唯一, 但他的查找速度是 O(N) + */ + name?: string + + /** + * "点"的物体单元类型, 最终对应到 measure / conveyor / task 等不同的单元处理逻辑中 + */ + t: string + + /** + * 变换矩阵, 3x3矩阵, 采用Y轴向上为正, X轴向右, Z轴向前的右手坐标系 + */ + tf: [ + /** + * 平移向量 position, 三维坐标 + */ + [number, number, number], + /** + * 旋转向量 rotation, 表示绕Y轴旋转的角度, 单位为度。对应 three.js 应进行"角度"转"弧度"的换算 + */ + [number, number, number], + /** + * 缩放向量 scale, 三维缩放比例 + */ + [number, number, number], + ] + + /** + * 用户数据, 可自定义, 一般用在 three.js 的 userData 中 + */ + dt: { + /** + * 标签名称, 显示用, 最后初始化到 three.js 的 userData.label 中, 最终应该如何渲染, 每个单元类型有自己的逻辑, 取决于物流单元类型t的 renderer 逻辑 + */ + label?: string + + /** + * 颜色, 最后初始化到 three.js 的 userData.color 中, 最终颜色应该如何渲染, 每个单元类型有自己的逻辑, 取决于物流单元类型t的 renderer 逻辑 + */ + color?: string + + /** + * S连线(又称逻辑连线), 与其他点之间的无方向性关联, 关系的起点需要在他的 dt.center[] 数组中添加目标点的id, 关系的终点需要在他的 dt.center[] 数组中添加起点的 id + */ + center?: string[] + /** + * A连线(又称物体流动线)的输入, 关系的终点需要在 dt.in[] 数组中添加起点的 id + */ + in?: string[] + /** + * A连线(又称物体流动线)的输出, 关系的起点需要在 dt.out[] 数组中添加目标点的 id + */ + out?: string[] + + /** + * 是否可以被选中, 默认 true + */ + selectable?: boolean + + /** + * 是否受保护, 不可在图形编辑器中拖拽, 默认 false + */ + protected?: boolean + + /** + * 其他自定义数据, 可以存储任何数据 + */ + [key: string]: any + }, +} +``` + + +## 场景和视窗 + +### 场景 SceneHelp + +```ts +/** + * 场景对象 + * 通常是某个楼层的所有物品和摆放, 每个场景可能会有多个不同的 Viewport 对他进行观察 + * 这是一个成熟的类, 不用对他改造 + */ +export default class SceneHelp { + scene: THREE.Scene + axesHelper: THREE.GridHelper + gridHelper: THREE.GridHelper + + /** + * 整个仓库的地图模型 + */ + worldModel: WorldModel + + /** + * 实体管理器, 所有控制实体都在这里管理 + */ + entityManager: EntityManager + + constructor(floor: string) +} +``` + + +### 状态管理器 DataStateManager + +```ts +/** + * 地图数据状态的管理器, 他能够对数据进行增删改查,并且能够进行撤销、重做等操作. + * 所有的修改都应该从这里发起, 多数修改都是从各个物流单元的 interaction 发起 + */ +export default class StateManager { + + /** + * 唯一场景标识符, 用于做临时存储的 key + */ + id: string + + /** + * 视口对象, 用于获取、同步当前场景的状态 + */ + viewport: Viewport + + /** + * 是否发生了变化,通知外部是否需要保存数据 + */ + isChanged = ref(false) + + /** + * 是否正在加载数据,通知外部是否需要等待加载完成 + */ + isLoading = ref(false) + + /** + * 当前场景数据 + */ + vdata: VData = { items: [], isChanged: false } + + constructor(id: string, viewport: Viewport) + + /** + * 开始用户操作(创建数据快照) + */ + beginUpdate() + + /** + * 结束用户操作(计算差异并保存), 内部会调用 syncDataState 方法, 换起实体管理器 EntityManager + */ + commitUpdate() + + /** + * 将当前数据 与 EntityManager 进行同步, 对比出不同的部分,分别进行更新 + * - 调用 viewport.entityManager.beginBatch() 开始更新 + * - 调用 viewport.entityManager.createEntity(vdataItem) 添加场景中新的实体 + * - 调用 viewport.entityManager.updateEntity(vdataItem) 新场景中已存在的实体 + * - 调用 viewport.entityManager.deleteEntity(id) 删除场景中的实体 + * - 调用 viewport.entityManager.commitBatch() 结束更新场景 + */ + syncDataState() + + /** + * 从外部加载数据 + */ + async load(items: VDataItem[]) + + /** + * 保存数据到外部 + */ + async save(): Promise + + /** + * 撤销 + */ + undo() + + /** + * 重做 + */ + redo() + + /** + * 保存到本地存储 浏览器indexDb(防止数据丢失) + */ + async saveToLocalstore() + + /** + * 从本地存储还原数据 + */ + async loadFromLocalstore() + + /** + * 删除本地存储 + */ + async removeLocalstore() +} +``` + + +### 视窗 Viewport + +```ts +/** + * 视窗对象, 这是一个成熟的类, 不用对他改造 + */ +export default class Viewport { + sceneHelp: SceneHelp + viewerDom: HTMLElement + camera: THREE.OrthographicCamera + renderer: THREE.WebGLRenderer + statsControls: Stats + controls: OrbitControls + raycaster: THREE.Raycaster + dragControl: EsDragControls + animationFrameId: any = null + + constructor(sceneHelp: SceneHelp, viewerDom: HTMLElement) + + /** + * 初始化 THREE 渲染器 + */ + initThree(sceneHelp: SceneHelp, viewerDom: HTMLElement, floor: string) + + /** + * 动画循环 + */ + animate() + + /** + * 销毁视窗 + */ + destroy() + + /** + * 获取坐标下所有对象 + */ + getIntersects(point: THREE.Vector2): THREE.Object3D[] + + /** + * 获取鼠标所在的 x,y,z 坐标 + * 鼠标坐标是相对于 canvas 元素 (renderer.domElement) 元素的 + */ + getClosestIntersection(e: MouseEvent) +} +``` + + +### 实体管理器 EntityManager + +```ts +/** + * 缓存所有实体和他们的关系, 在各个组件的渲染器会调用这个实体管理器, 进行检索 / 关系 / 获取差异等计算 + */ +export class EntityManager { + /** + * 视窗对象, 所有状态管理器, ThreeJs场景,控制器,摄像机, 实体管理器都在这里 + */ + viewport: Viewport + + // 所有数据点的实体 + entities = new Map(); + + // 所有关联关系 + relationIndex = new Map; + in: Set; + out: Set; + }>() + + // 两两关联关系与THREEJS对象之间的关联 + lines = new Map<[string, string], THREE.Object3D[]>(); + + constructor(viewport: Viewport) + + // 批量更新开始 + beginUpdate() + + // 创建一个实体, 这个点的 center[] / in[] / out[] 关联的点, 都要对应进行关联 + createEntity(entity: ItemJson, option?: EntityCudOption) + + // 更新实体, 他可能更新位置, 也可能更新颜色, 也可能修改 dt.center[] / dt.in[] / dt.out[] 修正与其他点之间的关联 + updateEntity(entity: ItemJson, option?: EntityCudOption) + + // 删除实体, 与这个点有关的所有线都要删除, 与这个点有关系的点,都要对应的 center[] / in[] / out[] 都要断绝关系 + deleteEntity(id: string, option?: EntityCudOption) + + // createEntity / updateEntity / deleteEntity 调整完毕之后, 调用这个方法进行收尾 + // 这个方法最重要的是进行连线逻辑的处理 + // - 如果进行了添加, 那么这个点的 center[] / in[] / out[] 关联的点, 都要对应进行关联 + // - 如果进行了删除, 与这个点有关的所有线都要删除, 与这个点有关系的点,都要对应的 center[] / in[] / out[] 都要断绝关系 + // - 如果进行了更新, 如果改了颜色/位置, 则需要在UI上进行对应修改,如果改了关系,需要与关联的节点批量调整 + // 将影响到的所有数据, 都变成一个修改集合, 统一调用对应单元类型渲染器(BaseRenderer)的 createPoint / deletePoint / updatePoint / createLine / updateLine / deleteLine 方法 + // 具体方法就是 viewport.getItemTypeRenderer(itemTypeName) + commitUpdate() + + // 获取实体 + getEntity(id: string): ItemJson | undefined + + // 获取相关实体 + getRelatedEntities(id: string, relationType: 'center' | 'in' | 'out'): ItemJson[] +} +``` + + + +## 物流单元封装 + +物流单元组件封装, 每个组件类别都会有 + +- 渲染器 renderer +- 交互控制器 interaction +- 实体定义 entity +- 属性元数据 meta + + +比如测量组件 Measure, 他属于最基础的线类型物流单元. + +### 渲染器 renderer + +```ts +/** + * 基本渲染器基类 + * 定义了点 / 线 该如何渲染到 ThreeJs 场景中, 这里可能会调用 InstancePool 进行渲染 + * 每个物流单元类型, 全局只有一个实例 + */ +public export default BaseRenderer { + + // 开始更新, 可能暂停动画循环对本渲染器的动画等 + beginUpdate(viewport: Viewport); + + // 创建一个点, 每种物流单元类型不一样, 可能 measure 是创建一个红色方块, 可能 moveline 创建一个菱形, 可能 conveyor 创建一个齿轮 + abstract createPoint(item: ItemJson, option?: RendererCudOption) + + // 删除一个点 + abstract deletePoint(id, option?: RendererCudOption); + + // 更新一个点 + abstract updatePoint(item: ItemJson, option?: RendererCudOption); + + // 创建一根线, 每种物流单元类型不同 这个方法都不同 + // 可能 measure 对于 in 和 out 忽略, center 就是创造一根 Line2, + // 可能 conveyor 对于 in 和 out 是创造一个 Mesh 并带动画 带纹理背景 带几何形状, center 就是画一个红色的细线 + abstract createLine(start: ItemJson, end: ItemJson, type: 'in' | 'out' | 'center', option?: RendererCudOption) + + // 更新一根线 + abstract updateLine(start: ItemJson, end: ItemJson, type: 'in' | 'out' | 'center', option?: RendererCudOption) + + // 删除一根线 + abstract deleteLine(start: ItemJson, end: ItemJson, option?: RendererCudOption) + + // 结束更新 + abstract endUpdate(viewport: Viewport); +} + +/** + * 辅助测量工具渲染器 + */ +public export default class MeasureRenderer extends BaseRenderer { + + // 开始更新, 可能暂停动画循环对本渲染器的动画等 + beginUpdate(viewport: Viewport); + + // 创建一个点 + createPoint(item: ItemJson, option?: RendererCudOption) + + // 删除一个点 + deletePoint(id, option?: RendererCudOption); + + // 更新一个点 + updatePoint(item: ItemJson, option?: RendererCudOption) + + // 创建一根线 + createLine(start: ItemJson, end: ItemJson, type: 'in' | 'out' | 'center', option?: RendererCudOption) + + // 更新一根线 + updateLine(start: ItemJson, end: ItemJson, type: 'in' | 'out' | 'center', option?: RendererCudOption) + + // 删除一根线 + deleteLine(start: ItemJson, end: ItemJson, option?: RendererCudOption) + + // 结束更新 + endUpdate(viewport: Viewport); +} +``` + +### 实例池 InstancePool + +```ts +{ + 不知道怎么做 +} +``` + + + +### 交互控制器 interaction + +```ts +/** + * 交互控制器基类 + * 定义了在建模编辑器里面物流单元, 如何响应鼠标, 键盘的操作. + * 每个物流单元类型, 全局只有一个实例 + */ +public default class BaseInteraction { + // 初始化 + init(viewport: Viewport) + + // 测量工具开始, 监听 Three.Renderer.domElement 的鼠标事件, 有可能用户会指定以某个点为起点, 也有可能第一个点由用户点击而来 + start(startPoint?: THREE.Object3D) + + // 用户鼠标移动时, 判断是否存在起点, 有起点就要实时构建临时线, 临时线的中间创建一个 Label 显示线的长度. 每次鼠标移动都要进行重新调整 + onMousemove(e: MouseEvent) + + // 用户如果点左键, 就调用 MeasureRenderer.createPoint 创建点, 如果点击右键就调用 Stop 退出工具 + onMouseup(e: MouseEvent) + + // 退出这个工具的点击, 停止对 Three.Renderer.domElement 的监听 + stop() + + // 用户在设计器拽动某个点时触发. 这时调整的都是 "虚点" 和 "虚线" + dragPointStart(point: THREE.Object3D) + + // 用户在设计器拖拽完毕后触发, 他会触发 MeasureRenderer.updatePoint 事件 + dragPointComplete() + + // 在用户开始测量工具 start / 或拖拽 dragPointStart 过程中,会创建临时点和临时线, 辅助用户 + // 临时点在一次交互中只会有一个 + createOrUpdateTempPoint(e: MouseEvent) + + // 临时线在一次交互中, 可能会有多个, 取决于调整的点 有多少关联点, 都要画出虚线和label + createOrUpdateTempLine(label: string, pointStart: THREE.Object3D, pointEnd: THREE.Object3D) + + // 清空所有临时点和线 + clearTemps() +} + +public default class MeasureInteraction extends BaseInteraction { + // 用户在设计器拽动某个点时触发. 这时调整的都是 "虚点" 和 "虚线" + dragPointStart(point: THREE.Object3D) + + // 用户在设计器拖拽完毕后触发, 他会触发 MeasureRenderer.updatePoint 事件 + dragPointComplete() +} +``` + + +### 属性元数据 meta + +```ts +/** + * 他定义了数据如何呈现在属性面板, 编辑器如果修改, 配合 Entity 该如何进行 + */ +export default { + // "点"属性面板 + point: { + // 基础面板 + basic: [ + { field: 'uuid', editor: 'UUID', label: 'uuid', readonly: true }, + { field: 'name', editor: 'TextInput', label: '名称' }, + { field: 'dt.label', editor: 'TextInput', label: '标签' }, + { editor: 'TransformEditor' }, + { field: 'dt.color', editor: 'Color', label: '颜色' }, + { editor: '-' }, + { field: 'tf', editor: 'InOutCenterEditor' }, + { field: 'dt.selectable', editor: 'Switch', label: '可选中' }, + { field: 'dt.protected', editor: 'Switch', label: '受保护' }, + { field: 'visible', editor: 'Switch', label: '可见' } + ] + }, + // "线"属性面板 + line: { + ...xxx + } +} +``` + + +### 实体定义 entity + +```ts +/** + * 基本"点"属性操作代理实体 + * 这个对象不易大量存在, 只有在绑定控制面板, 高级属性对话框, 或操作 Modeltree, 或脚本控制的时候才被实例化 + * 他提供操作的抽象代理, 最终可能调用 DataStateManager 进行数据的保存 + */ +export default class BaseItemEntity { + setItem(itemJson: ItemJson) + setObject(point: THREE.Object3D) +} + + +/** + * 基本"线"属性操作代理实体 + * 这个对象不易大量存在, 只有在绑定控制面板, 高级属性对话框, 或操作 Modeltree, 或脚本控制的时候提供Proxy操作代理 + */ +export default class BaseLineEntity { + setItem(start: ItemJson, end: ItemJson) + setObjects(line: THREE.Object3D[]) +} + +/** + * "线"属性 操作代理实体 + */ +export class MeasurePoint : BaseItemEntity { + ... 各种 get / set / method +} + +/** + * "线"属性 操作代理实体 + */ +export class MeasureLine: BaseLineEntity { + ... 各种 get / set / method +} +``` + + +所有的模型操作都遵循如流程 + +DataStateManager -> EntityManager -> xxx.renderer -> InstancePool + + +辅助类的操作则是通过交互控制器 + +interaction 完成临时辅助对象的创建, 辅助线 / 临时单元 是不会被持久化的 + + +点之间的关系不会非常复杂, 通常是比较稀疏的, 可能一个点最多有6个连线, 绝大部分点 只有1~2个关系连线. + +在大规模制图, 比如单场景中存在 10000 个以上的点, 是否存在性能问题? + +这种封装是否合理, 有什么优化建议? \ No newline at end of file diff --git a/src/assets/images/conveyor/shapes/logo.png b/src/assets/images/conveyor/shapes/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6a58f485e0c54d8eee439d79d7bf09b6a5106879 GIT binary patch literal 219515 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

ohe|xV-lbhfH+sg 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Ν!c0YD&?+a|Q$kP78-RRFD~t){b- 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%0D#8|V&Se8~BngttGZ6JC9C)Jx9%UPSw?pH1) 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 literal 0 HcmV?d00001 diff --git a/src/components/Model3DView.vue b/src/components/Model3DView.vue new file mode 100644 index 0000000..e5b7ecf --- /dev/null +++ b/src/components/Model3DView.vue @@ -0,0 +1,921 @@ + + + \ No newline at end of file diff --git a/src/core/Constract.ts b/src/core/Constract.ts new file mode 100644 index 0000000..afd4a7e --- /dev/null +++ b/src/core/Constract.ts @@ -0,0 +1,20 @@ +export default Object.freeze({ + // 光标相关 + CursorModeNormal: 'normal', + + // 关联相关 + CursorModeALink: 'ALink', + CursorModeSLink: 'SLink', + CursorModePointCallback: 'PointCallback', + CursorModePointAdd: 'PointAdd', + CursorModeLinkAdd: 'LinkAdd', + CursorModeLinkAdd2: 'LinkAdd2', + + // 测量相关的光标模式 + CursorModeMeasure: 'measure', + + CursorModeConveyor: 'conveyor', + + // 选择模式 + CursorModeSelectByRec: 'selectByRec' +}) \ No newline at end of file diff --git a/src/core/ModelUtils.ts b/src/core/ModelUtils.ts new file mode 100644 index 0000000..4ea6e35 --- /dev/null +++ b/src/core/ModelUtils.ts @@ -0,0 +1,292 @@ +import * as THREE from 'three' +import type { ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine.ts' +import { getAllItemTypes, getItemTypeByName } from '@/model/itemType/ItemTypeDefine.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() { + 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() { + // 按下 ESC 键,取消当前操作 + const viewport: Viewport = window['viewport'] + if (!viewport) { + system.msg('没有找到当前视图') + return + } + + viewport.state.cursorMode = 'normal' + system.msg('操作已取消') +} + +export function quickCopyByMouse() { + // 获取鼠标位置,查看鼠标是否在某个 viewport 的画布上,并取得该 viewport + const currentMouseInfo = window['CurrentMouseInfo'] + if (!currentMouseInfo?.viewport || !currentMouseInfo.x || !currentMouseInfo.z) { + system.msg('无法获取鼠标位置') + return + } + + const x = currentMouseInfo.x + const z = currentMouseInfo.z + const viewport: Viewport = currentMouseInfo.viewport + // const point: THREE.Vector2 = currentMouseInfo.mouse + // + // const ray = new THREE.Raycaster() + // ray.setFromCamera(point, viewport.camera) + // const intersections = ray.intersectObjects(viewport.dragControl._dragObjects, true) + // + // if (intersections.length === 0) { + // system.msg('没有找到可复制的对象') + // return + // } + // console.log('intersections:', intersections) + + // 如果不在线上,查找0.2米内的有效点 Object3D, 如果有,则以这个点为起点, 延伸同类型的点,并让他们相连 + const r = findObject3DByCondition(viewport.scene, object => { + // 判断 object 是否是有效的 Object3D, 并且是当前 viewport 的对象 + if (object instanceof THREE.Object3D && object.visible && + object.userData.type && viewport.toolbox[object.userData.type]) { + + const toolbox: Toolbox = viewport.toolbox[object.userData.type] + + // 检查是否在 0.2 米内 + const distance = object.position.distanceTo(new THREE.Vector3(x, 0, z)) + if (distance < 0.2) { + // 找到一个有效点,执行复制操作 + viewport.toolStartObject = object + viewport.state.cursorMode = object.userData.type + // toolbox.start(object) + system.msg('连线成功') + return true + } + } + return false + }) + + if (!r || r.length === 0) { + system.msg('鼠标所在位置,没有可复制的对象') + return + } +} + +// +// /** +// * 查找射线周围指定半径内的对象 +// */ +// export function findObjectsInRadius(viewport: Viewport, +// point: THREE.Vector2, +// radius: number, +// lines: { object: THREE.Object3D, distance: number }[], +// points: { object: THREE.Object3D, distance: number }[] +// ): void { +// const ray = new THREE.Raycaster() +// ray.setFromCamera(point, viewport.camera) +// +// viewport.dragControl._dragObjects.forEach(obj => { +// if (obj instanceof THREE.Points) { +// // 处理点云:遍历每个点 +// const distance = distanceToRay(ray, point) +// if (distance <= radius) { +// points.push({ object: obj, distance }) +// } +// +// } else if (obj instanceof THREE.Line) { +// // 处理线段:计算线段到射线的最近距离 +// const distance = getLineDistanceToRay(ray, obj) +// if (distance <= radius) { +// lines.push({ object: obj, distance }) +// } +// } +// }) +// } +// +// /** +// * 计算点到射线的最短距离 +// */ +// function distanceToRay(ray: THREE.Raycaster, point: THREE.Vector2) { +// const closestPoint = new THREE.Vector3() +// ray.closestPointToPoint(point, closestPoint) +// return point.distanceTo(closestPoint) +// } +// +// /** +// * 计算线段到射线的最短距离 +// */ +// function getLineDistanceToRay(ray: THREE.Raycaster, line: THREE.Line) { +// const lineStart = new THREE.Vector3() +// const lineEnd = new THREE.Vector3() +// line.geometry.attributes.position.getXYZ(0, lineStart) +// line.geometry.attributes.position.getXYZ(1, lineEnd) +// line.localToWorld(lineStart) +// line.localToWorld(lineEnd) +// +// const lineSegment = new THREE.Line3(lineStart, lineEnd) +// const closestOnRay = new THREE.Vector3() +// const closestOnLine = new THREE.Vector3() +// THREE.Line3.prototype.closestPointsRayLine ??= function(ray, line, closestOnRay, closestOnLine) { +// // 实现射线与线段最近点计算(需自定义或使用数学库) +// } +// +// lineSegment.closestPointsRayLine(ray, true, closestOnRay, closestOnLine) +// return closestOnRay.distanceTo(closestOnLine) +// } + +/** + * 考虑吸附的情况下计算鼠标事件位置 + */ +export function calcPositionUseSnap(e: MouseEvent, point: THREE.Vector3) { + // 按下 ctrl 键,不启用吸附,其他情况启用吸附 + const gridOption = worldModel.gridOption + if (!e.ctrlKey && !e.metaKey) { + if (gridOption.snapEnabled && gridOption.snapDistance > 0) { + // 启用吸附, 针对 point 的 x 和 z 坐标进行吸附, 吸附距离为 gridOption.snapDistance + const snapDistance = gridOption.snapDistance + const newPoint = new THREE.Vector3(point.x, point.y, point.z) + newPoint.x = Math.round(newPoint.x / snapDistance) * snapDistance + newPoint.z = Math.round(newPoint.z / snapDistance) * snapDistance + return newPoint + } + } + + return point +} + +export function getAllControlPoints(): THREE.Object3D[] { + const allPoints: THREE.Object3D[] = [] + + getAllItemTypes().forEach((itemType: ItemTypeDefineOption) => { + if (itemType.clazz && itemType.clazz.pointArray) { + // 将每个 ItemType 的点添加到结果数组中 + allPoints.push(...itemType.clazz.pointArray) + } + }) + + return allPoints +} + +/** + * 在给定的场景中查找具有指定 uuid 的 Object3D 对象 + */ +export function findObject3DById(scene: THREE.Object3D, uuid: string): THREE.Object3D | undefined { + const rets = findObject3DByCondition(scene, object => object.uuid === uuid) + if (rets.length > 0) { + return rets[0] + } + return undefined +} + +/** + * 在给定场景中查找满足特定条件的 Object3D 对象集合 + */ +export function findObject3DByCondition(scene: THREE.Object3D, condition: (object: THREE.Object3D) => boolean): THREE.Object3D[] { + const foundObjects: THREE.Object3D[] = [] + + // 定义一个内部递归函数来遍历每个节点及其子节点 + function traverse(obj: THREE.Object3D) { + if (condition(obj)) { + foundObjects.push(obj) + } + + // 遍历当前对象的所有子对象 + for (let i = 0; i < obj.children.length; i++) { + traverse(obj.children[i]) + } + } + + // 开始从场景根节点进行遍历 + traverse(scene) + + return foundObjects +} + +export function loadSceneFromJson(viewport: Viewport, scene: THREE.Scene, items: ItemJson[]) { + console.time('loadSceneFromJson') + + const object3ds: THREE.Object3D[] = [] + + // beforeLoad 通知所有加载的对象, 模型加载开始 + getAllItemTypes().forEach((itemType: ItemTypeDefineOption) => { + const ret = itemType.clazz.beforeLoad() + Array.isArray(ret) && object3ds.push(...ret) + }) + + const loads = loadObject3DFromJson(items) + Array.isArray(loads) && object3ds.push(...loads) + + // afterLoadComplete 通知所有加载的对象, 模型加载完成 + getAllItemTypes().forEach((itemType: ItemTypeDefineOption) => { + const ret = itemType.clazz.afterLoadComplete(object3ds) + Array.isArray(ret) && object3ds.push(...ret) + }) + + scene.add(...object3ds) + + // afterAddScene 通知所有加载的对象, 模型加载完成 + getAllItemTypes().forEach(itemType => { + itemType.clazz.afterAddScene(viewport, scene, object3ds) + }) + + console.log('loadSceneFromJson:', items.length, 'items,', object3ds.length, 'objects') + console.timeEnd('loadSceneFromJson') +} + +function loadObject3DFromJson(items: ItemJson[]): THREE.Object3D[] { + const result: THREE.Object3D[] = [] + + for (const item of items) { + if (!item || !item.t) { + console.error('unkown item:', item) + continue + } + + const object3D: THREE.Object3D | undefined = getItemTypeByName(item.t)?.clazz.loadFromJson(item) + if (object3D === undefined) { + continue + } + + if (_.isArray(item.items)) { + // 如果有子元素,递归处理 + const children = loadObject3DFromJson(item.items) + children.forEach(child => object3D.add(child)) + } + + result.push(object3D) + } + + return result +} \ No newline at end of file diff --git a/src/core/base/BaseInteraction.ts b/src/core/base/BaseInteraction.ts new file mode 100644 index 0000000..d3b1bb5 --- /dev/null +++ b/src/core/base/BaseInteraction.ts @@ -0,0 +1,35 @@ +import * as THREE from 'three' +import type Viewport from '@/core/engine/Viewport' + +/** + * 基本交互控制器基类 + * 定义了在建模编辑器中物流单元如何响应鼠标和键盘操作 + */ +export default abstract class BaseInteraction { + protected viewport!: Viewport + + /** + * 开始交互 + * @param viewport 当前视口 + * @param startPoint 起点对象(可选) + */ + abstract start(viewport: Viewport, startPoint?: THREE.Object3D): void + + /** + * 停止交互 + */ + abstract stop(): void + + /** + * 拖拽点开始 + * @param viewport 当前视口 + * @param point 拖拽的点 + */ + abstract dragPointStart(viewport: Viewport, point: THREE.Object3D): void + + /** + * 拖拽点完成 + * @param viewport 当前视口 + */ + abstract dragPointComplete(viewport: Viewport): void +} \ No newline at end of file diff --git a/src/core/base/BaseItemEntity.ts b/src/core/base/BaseItemEntity.ts new file mode 100644 index 0000000..a048ffe --- /dev/null +++ b/src/core/base/BaseItemEntity.ts @@ -0,0 +1,26 @@ +import * as THREE from 'three' + +/** + * BaseEntity class + * Provides a base for managing logistics unit entities. + */ +export default abstract class BaseEntity { + protected itemJson!: ItemJson + protected objects!: THREE.Object3D[] + + /** + * Sets the `ItemJson` data for the entity. + * @param itemJson - The `ItemJson` data to set. + */ + setItem(itemJson: ItemJson): void { + this.itemJson = itemJson + } + + /** + * Sets the `THREE.Object3D` object for the entity. + * @param object3D - The `THREE.Object3D` object to set. + */ + setObjects(objects: THREE.Object3D[]): void { + this.objects = objects + } +} \ No newline at end of file diff --git a/src/core/base/BaseRenderer.ts b/src/core/base/BaseRenderer.ts new file mode 100644 index 0000000..4e50a48 --- /dev/null +++ b/src/core/base/BaseRenderer.ts @@ -0,0 +1,85 @@ +import type Viewport from '@/core/engine/Viewport' + +/** + * 基本渲染器基类 + * 定义了点 / 线如何渲染到 Three.js 场景中 + */ +export default abstract class BaseRenderer { + /** + * 开始更新 + * @param viewport 当前视口 + */ + beginUpdate(viewport: Viewport): void { + // Optional: Pause animations or prepare for batch updates + } + + /** + * 创建一个点 + * @param item 点的定义 + * @param option 渲染选项 + */ + abstract createPoint(item: ItemJson, option?: RendererCudOption): void + + /** + * 删除一个点 + * @param id 点的唯一标识 + * @param option 渲染选项 + */ + abstract deletePoint(id: string, option?: RendererCudOption): void + + /** + * 更新一个点 + * @param item 点的定义 + * @param option 渲染选项 + */ + abstract updatePoint(item: ItemJson, option?: RendererCudOption): void + + /** + * 创建一根线 + * @param start 起点 + * @param end 终点 + * @param type 线的类型 + * @param option 渲染选项 + */ + abstract createLine( + start: ItemJson, + end: ItemJson, + type: 'in' | 'out' | 'center', + option?: RendererCudOption + ): void + + /** + * 更新一根线 + * @param start 起点 + * @param end 终点 + * @param type 线的类型 + * @param option 渲染选项 + */ + abstract updateLine( + start: ItemJson, + end: ItemJson, + type: 'in' | 'out' | 'center', + option?: RendererCudOption + ): void + + /** + * 删除一根线 + * @param start 起点 + * @param end 终点 + * @param option 渲染选项 + */ + abstract deleteLine( + start: ItemJson, + end: ItemJson, + option?: RendererCudOption + ): void + + /** + * 结束更新 + * @param viewport 当前视口 + */ + endUpdate(viewport: Viewport): void { + // Optional: Resume animations or finalize batch updates + } +} + diff --git a/src/core/base/IMeta.ts b/src/core/base/IMeta.ts new file mode 100644 index 0000000..b9bb8d3 --- /dev/null +++ b/src/core/base/IMeta.ts @@ -0,0 +1,55 @@ +import type { ItemTypeMeta } from '@/model/itemType/ItemTypeDefine.ts' + +/** + * "点"对象类型的,基础元数据 + */ +export const BASIC_META_OF_POINT: ItemTypeMeta = [ + { field: 'uuid', editor: 'UUID', label: 'uuid', readonly: true }, + { field: 'name', editor: 'TextInput', label: '名称' }, + { field: 'userData.label', editor: 'TextInput', label: '标签' }, + { editor: 'Transform' }, + { field: 'color', editor: 'Color', label: '颜色' }, + { editor: '-' }, + { editor: 'IN_OUT_CENTER' } +] + +/** + * "物流运输单元"对象类型的,基础元数据, 排在后面的 + */ +export const BASIC_META_OF_POINT2: ItemTypeMeta = [ + { field: 'userData.selectable', editor: 'Switch', label: '可选中' }, + { field: 'userData.protected', editor: 'Switch', label: '受保护' }, + { field: 'visible', editor: 'Switch', label: '可见' } +] + +/** + * "线"对象类型的,基础元数据 + */ +export const BASIC_META_OF_LINE: ItemTypeMeta = [] + +/** + * "线"对象类型的,基础元数据, 排在后面的 + */ +export const BASIC_META_OF_LINE2: ItemTypeMeta = [] + +/** + * 属性面板元数据声明, 第一级 category, 第二级 tabName, 第三级 MetaItem + */ +export interface IMeta { + [key: string]: { + [tabName: string]: MetaItem[] + } +} + +/** + * PropertyPanelConfig interface + * Defines the structure of property panel configurations. + */ +export interface MetaItem { + field?: string; + editor: string; + label?: string; + readonly?: boolean; + + [key: string]: any; +} \ No newline at end of file diff --git a/src/core/controls/DragControls.js b/src/core/controls/DragControls.js new file mode 100644 index 0000000..a4a7c74 --- /dev/null +++ b/src/core/controls/DragControls.js @@ -0,0 +1,209 @@ +import { + EventDispatcher, + Matrix4, + Plane, + Raycaster, + Vector2, + Vector3 +} from 'three' +import { calcPositionUseSnap } from '@/core/ModelUtils.js' + +const _plane = new Plane() +const _raycaster = new Raycaster() + +const _pointer = new Vector2() +const _offset = new Vector3() +const _intersection = new Vector3() +const _worldPosition = new Vector3() +const _inverseMatrix = new Matrix4() + +class DragControls extends EventDispatcher { + constructor(_objects, _camera, _domElement) { + super() + + _domElement.style.touchAction = 'none' // disable touch scroll + + let _selected = null, _hovered = null + + const _intersections = [] + + // + + let isMove = false + let isMouseDownClicked = false + const scope = this + + function activate() { + _domElement.addEventListener('pointermove', onPointerMove) + _domElement.addEventListener('pointerdown', onPointerDown) + _domElement.addEventListener('pointerup', onPointerCancel) + _domElement.addEventListener('pointerleave', onPointerCancel) + } + + function deactivate() { + _domElement.removeEventListener('pointermove', onPointerMove) + _domElement.removeEventListener('pointerdown', onPointerDown) + _domElement.removeEventListener('pointerup', onPointerCancel) + _domElement.removeEventListener('pointerleave', onPointerCancel) + + _domElement.style.cursor = '' + } + + function dispose() { + deactivate() + } + + function setObjects(objects) { + _objects = objects + } + + function getObjects() { + return _objects + } + + function getRaycaster() { + return _raycaster + } + + function onPointerMove(event) { + if (!scope.enabled || !scope.enabledMove) return + + if (isMouseDownClicked) { + _domElement.style.cursor = 'move' + } + + isMove = true + updatePointer(event) + _raycaster.setFromCamera(_pointer, _camera) + + if (_selected) { + if (_raycaster.ray.intersectPlane(_plane, _intersection)) { + const pos = _intersection.sub(_offset).applyMatrix4(_inverseMatrix) + const newIntersection = calcPositionUseSnap(event, pos) + _selected.position.copy(newIntersection) + } + scope.dispatchEvent({ type: 'drag', object: _selected }) + return + } + + // hover support + if (event.pointerType === 'mouse' || event.pointerType === 'pen') { + + _intersections.length = 0 + + _raycaster.setFromCamera(_pointer, _camera) + _raycaster.intersectObjects(_objects, true, _intersections) + + if (_intersections.length > 0) { + + const object = _intersections[0].object + + _plane.setFromNormalAndCoplanarPoint(_camera.getWorldDirection(_plane.normal), _worldPosition.setFromMatrixPosition(object.matrixWorld)) + + if (_hovered !== object && _hovered !== null) { + + scope.dispatchEvent({ type: 'hoveroff', object: _hovered }) + + _domElement.style.cursor = 'auto' + _hovered = null + + } + + if (_hovered !== object) { + + scope.dispatchEvent({ type: 'hoveron', object: object }) + + _domElement.style.cursor = 'pointer' + _hovered = object + + } + + } else { + + if (_hovered !== null) { + scope.dispatchEvent({ type: 'hoveroff', object: _hovered }) + _domElement.style.cursor = 'auto' + _hovered = null + } + + } + + } + + } + + function onPointerDown(event) { + if (scope.enabled === false) return + + updatePointer(event) + + _intersections.length = 0 + + _raycaster.setFromCamera(_pointer, _camera) + let objects = _objects + + _raycaster.intersectObjects(objects, true, _intersections) + + if (_intersections.length > 0) { + _selected = (scope.transformGroup === true) ? _objects[0] : _intersections[0].object + + if (scope.enabledMove) { + _plane.setFromNormalAndCoplanarPoint(_camera.getWorldDirection(_plane.normal), _worldPosition.setFromMatrixPosition(_selected.matrixWorld)) + if (_raycaster.ray.intersectPlane(_plane, _intersection)) { + _inverseMatrix.copy(_selected.parent.matrixWorld).invert() + _offset.copy(_intersection).sub(_worldPosition.setFromMatrixPosition(_selected.matrixWorld)) + } + + // setTimeout(() => { + // _domElement.style.cursor = 'move' + // }, 20) + isMouseDownClicked = true + } + scope.dispatchEvent({ type: 'dragstart', object: _selected, e: event }) + } + + isMove = false + } + + function onPointerCancel(event) { + if (scope.enabled === false) return + + if (_selected) { + scope.dispatchEvent({ type: 'dragend', object: _selected, e: event }) + _selected = null + } else if (!isMove) { + // 添加点击空白处的事件 + scope.dispatchEvent({ type: 'clickblank', e: event }) + } + + _domElement.style.cursor = _hovered ? 'pointer' : 'auto' + isMouseDownClicked = false + } + + function updatePointer(event) { + const rect = _domElement.getBoundingClientRect() + + _pointer.x = (event.clientX - rect.left) / rect.width * 2 - 1 + _pointer.y = -(event.clientY - rect.top) / rect.height * 2 + 1 + } + + activate() + + // API + + this.enabled = true + this.enabledMove = true + this.transformGroup = false + + this.activate = activate + this.deactivate = deactivate + this.dispose = dispose + this.setObjects = setObjects + this.getObjects = getObjects + this.getRaycaster = getRaycaster + + } + +} + +export { DragControls } diff --git a/src/core/controls/EsDragControls.ts b/src/core/controls/EsDragControls.ts new file mode 100644 index 0000000..01b718f --- /dev/null +++ b/src/core/controls/EsDragControls.ts @@ -0,0 +1,155 @@ +import * as THREE from 'three' +import { DragControls } from './DragControls.js' +import type Viewport from '@/core/engine/Viewport.ts' +import { getItemTypeByName } from '@/model/itemType/ItemTypeDefine' +import type { ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine' +import { markRaw } from 'vue' +import EventBus from '@/runtime/EventBus' + +// dragControls 绑定函数 +let dragStartFn, dragFn, dragEndFn, clickblankFn + +export default class EsDragControls { + _dragObjects: THREE.Object3D[] = [] // 拖拽对象 + dragControls: any + private onDownPosition: { x: number; y: number } = { x: -1, y: -1 } + + viewport: Viewport + isDragging = false + + constructor(viewport) { + this.viewport = viewport + + // 物体拖拽控制器 + this.dragControls = new DragControls(this._dragObjects, viewport.camera, viewport.renderer.domElement) + this.dragControls.deactivate() // 默认禁用 + dragStartFn = this.dragControlsStart.bind(this) + this.dragControls.addEventListener('dragstart', dragStartFn) + dragFn = this.drag.bind(this) + this.dragControls.addEventListener('drag', dragFn) + dragEndFn = this.dragControlsEnd.bind(this) + this.dragControls.addEventListener('dragend', dragEndFn) + // 点击可拖拽物体之外 + clickblankFn = this.clickblank.bind(this) + this.dragControls.addEventListener('clickblank', clickblankFn) + } + + set domElement(element: HTMLElement) { + this.dragControls.setDomElement(element) + } + + setDragObjects(objects: THREE.Object3D[], type: 'eq' | 'push' | 'remove' = 'eq') { + // 当前拖拽对象为空时加入对象需激活控制器 + if (this._dragObjects.length === 0) { + if (objects.length > 0) { + this.dragControls.activate() + } + + this._dragObjects = objects + } else { + // 当前拖拽对象不为空时 + if (type === 'eq') { + // 是清空拖拽对象的设置,则禁用控制器 + if (objects.length === 0) { + this.dragControls.deactivate() + } + + this._dragObjects = objects + } else if (type === 'push') { + this._dragObjects.push(...objects) + } else if (type === 'remove') { + this._dragObjects = this._dragObjects.filter((item) => !objects.includes(item)) + } + } + + this.dragControls.setObjects(this._dragObjects) + } + + // 拖拽开始 + dragControlsStart(e) { + // 右键拖拽不响应 + if (e.e.button === 2 || !e.object.userData.type || !e.object.visible) return + + e.e.preventDefault() + + // 拖拽时禁用其他控制器 + this.viewport.controls.enabled = false + + this.isDragging = true + + // 记录拖拽按下的位置和对象 + this.onDownPosition = { x: e.e.clientX, y: e.e.clientY } + + if (e.object.userData?.type) { + const itemType: ItemTypeDefineOption = getItemTypeByName(e.object.userData.type) + if (itemType?.clazz) { + itemType.clazz.dragPointStart(this.viewport, e.object) + } + } + // switch (e.object.userData.type) { + // case Constract.MeasureMarker: + // this.viewport.measure.dragPointStart(e.object) + // break + // } + } + + // 拖拽中 + drag(e) { + EventBus.dispatch('objectChanged', { + viewport: this, + object: e.object + }) + } + + // 拖拽结束 + dragControlsEnd(e) { + // 右键拖拽不响应 + if (e.e.button === 2 || !e.object.visible) return + + // 拖拽结束启用其他控制器 + this.viewport.controls.enabled = true + + this.isDragging = false + + if (!e.object.userData.type) return + + // 判断位置是否有变化,没有变化则为点击 + if (this.onDownPosition.x === e.e.clientX && this.onDownPosition.y === e.e.clientY) { + if (e.object.userData.onClick) { + e.object.userData.onClick(e) + } + if (e.object.userData.selectable) { + this.viewport.state.selectedObject = markRaw(e.object) + EventBus.dispatch('objectChanged', { + viewport: this, + object: e.object + }) + } + } + + if (e.object.userData?.type) { + const itemType: ItemTypeDefineOption = getItemTypeByName(e.object.userData.type) + if (itemType?.clazz) { + itemType.clazz.dragPointComplete(this.viewport) + } + } + // switch (e.object.userData.type) { + // case Constract.MeasureMarker: + // this.viewport.measure.dragPointComplete() + // break + // } + } + + // 点击可拖拽物体之外 + clickblank(e) { + if (e.e.button === 2) return + } + + dispose() { + this._dragObjects = [] + + this.dragControls.removeEventListener('dragstart', dragStartFn) + this.dragControls.removeEventListener('dragend', dragEndFn) + this.dragControls.dispose() + } +} \ No newline at end of file diff --git a/src/core/controls/IControls.ts b/src/core/controls/IControls.ts new file mode 100644 index 0000000..2476375 --- /dev/null +++ b/src/core/controls/IControls.ts @@ -0,0 +1,7 @@ +export default interface IControls { + init(viewport: any): void + + destory(): void + + animate?: () => void; +} \ No newline at end of file diff --git a/src/core/controls/MouseMoveInspect.ts b/src/core/controls/MouseMoveInspect.ts new file mode 100644 index 0000000..27ea283 --- /dev/null +++ b/src/core/controls/MouseMoveInspect.ts @@ -0,0 +1,76 @@ +import type Viewport from '@/core/engine/Viewport' +import type IControls from './IControls' +import * as THREE from 'three' + +let pmFn, otFn, lvFn + +/** + * 鼠标移动时,将鼠标位置的坐标转换为设计图上的坐标,并设置到 designer.mousePos 属性中 + */ +export default class MouseMoveInspect implements IControls { + viewport: Viewport + canvas: HTMLCanvasElement + + constructor() { + } + + init(viewport: Viewport) { + this.viewport = viewport + this.canvas = this.viewport.renderer.domElement as HTMLCanvasElement + + pmFn = this.mouseMove.bind(this) + otFn = this.mouseLv.bind(this) + lvFn = this.mouseLv.bind(this) + this.canvas.addEventListener('pointermove', pmFn) + this.canvas.addEventListener('pointerout', otFn) + this.canvas.addEventListener('mouseleave', lvFn) + } + + destory() { + this.canvas.removeEventListener('pointermove', pmFn) + pmFn = undefined + this.canvas.removeEventListener('pointerout', otFn) + otFn = undefined + this.canvas.removeEventListener('mouseleave', lvFn) + lvFn = undefined + } + + mouseLv() { + this.viewport.state.mouse.x = 0 + this.viewport.state.mouse.z = 0 + window['CurrentMouseInfo'] = { + x: 0, + z: 0 + } + } + + mouseMove = _.throttle(function(this: MouseMoveInspect, event: MouseEvent) { + + const pointv = new THREE.Vector2() + pointv.x = event.offsetX / this.viewport.renderer.domElement.offsetWidth + pointv.y = event.offsetY / this.viewport.renderer.domElement.offsetHeight + + const mouse = new THREE.Vector2() + mouse.set((pointv.x * 2) - 1, -(pointv.y * 2) + 1) + + // 当前鼠标所在的点 + const point = this.viewport.getClosestIntersection(event) + if (!point) { + return + } + + this.viewport.state.mouse.x = point.x + this.viewport.state.mouse.z = point.z + + window['CurrentMouseInfo'] = { + viewport: this.viewport, + x: point.x, + z: point.z, + mouse: mouse + } + + }, 1) + + animate(): void { + } +} \ No newline at end of file diff --git a/src/core/controls/SelectInspect.ts b/src/core/controls/SelectInspect.ts new file mode 100644 index 0000000..9024857 --- /dev/null +++ b/src/core/controls/SelectInspect.ts @@ -0,0 +1,213 @@ +import * as THREE from 'three' +import type IControls from './IControls' +import { watch } from 'vue' +import type Viewport from '@/core/engine/Viewport' +import { Line2 } from 'three/examples/jsm/lines/Line2.js' +import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js' +import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js' +import { getItemTypeByName } from '@/model/itemType/ItemTypeDefine.ts' +import EventBus from '@/runtime/EventBus' + +let pdFn, pmFn, puFn + +/** + * 选择工具,用于在设计器中显示选中对象的包围盒 + */ +export default class SelectInspect implements IControls { + viewport: Viewport + /** + * 线框材质,用于显示选中对象的包围盒 + */ + material: LineMaterial = new LineMaterial({ color: 0xffff00, linewidth: 2 }) + + /** + * 矩形材质,用于显示鼠标拖拽选择的矩形区域 + */ + rectMaterial: THREE.MeshBasicMaterial = new THREE.MeshBasicMaterial({ + color: 0x000000, + opacity: 0.3, + transparent: true + }) + + /** + * 当前选中对象的矩形选择框 + */ + rectangle: THREE.Mesh | null = null + + /** + * 当前选中对象的包围盒线框 + */ + selectionBox: Line2 + + /** + * 当前鼠标所在的画布, 对应 viewport.renderer.domElement + */ + canvas: HTMLCanvasElement + + /** + * 鼠标按下时记录的起始位置,用于绘制矩形选择框 + */ + recStartPos: THREE.Vector3 | null + + constructor() { + } + + init(viewport: Viewport) { + this.viewport = viewport + this.canvas = this.viewport.renderer.domElement as HTMLCanvasElement + + pdFn = this.onMouseDown.bind(this) + this.canvas.addEventListener('pointerdown', pdFn) + pmFn = this.onMouseMove.bind(this) + this.canvas.addEventListener('pointermove', pmFn) + puFn = this.onMouseUp.bind(this) + this.canvas.addEventListener('pointerup', puFn) + + this.viewport.watchList.push(watch(() => this.viewport.state.selectedObject, this.updateSelectionBox.bind(this))) + EventBus.on('objectChanged', (data) => { + this.updateSelectionBox(this.viewport.state.selectedObject) + }) + } + + /** + * 更新选中对象的包围盒线框 + */ + updateSelectionBox(selectedObject: THREE.Object3D) { + this.disposeSelectionBox() + this.viewport.state.selectedObjectMeta = null // 清除之前的元数据 + + if (selectedObject?.userData?.type) { + const type = selectedObject.userData.type + const itemTypeDefine = getItemTypeByName(type) + if (itemTypeDefine) { + this.viewport.state.selectedObjectMeta = itemTypeDefine.getMeta(selectedObject) + + const expandAmount = 0.2 // 扩展包围盒的大小 + // 避免某些蒙皮网格的帧延迟效应(e.g. Michelle.glb) + selectedObject.updateWorldMatrix(false, true) + + const box = new THREE.Box3().setFromObject(selectedObject) + box.expandByScalar(expandAmount) + + const size = new THREE.Vector3() + box.getSize(size) + + const center = new THREE.Vector3() + box.getCenter(center) + + // 创建包围盒几何体 + const helperGeometry = new THREE.BoxGeometry(size.x, size.y, size.z) + const edgesGeometry = new THREE.EdgesGeometry(helperGeometry) + + // 使用 LineGeometry 包装 edgesGeometry + const lineGeom = new LineGeometry() + //@ts-ignore + lineGeom.setPositions(edgesGeometry.attributes.position.array) + + const selectionBox = new Line2(lineGeom, this.material) + selectionBox.computeLineDistances() + selectionBox.position.copy(center) + selectionBox.name = 'selectionBox' + this.selectionBox = selectionBox + + this.viewport.scene.add(selectionBox) + + } + } + } + + destory() { + + this.canvas.removeEventListener('pointerdown', pdFn) + pdFn = undefined + this.canvas.removeEventListener('pointermove', pmFn) + pmFn = undefined + this.canvas.removeEventListener('pointerup', puFn) + puFn = undefined + + // 销毁选择工具 + this.disposeSelectionBox() + this.disposeRect() + } + + disposeSelectionBox() { + if (this.selectionBox) { + this.viewport.scene.remove(this.selectionBox) + this.selectionBox.geometry.dispose() + this.selectionBox = null + } + } + + createRectangle() { + if (this.rectangle !== null) { + this.disposeRect() + } + if (this.recStartPos) { + // 创建矩形 + this.rectangle = new THREE.Mesh( + new THREE.PlaneGeometry(1, 1), + this.rectMaterial + ) + this.rectangle.name = 'selectRectangle' + this.rectangle.rotation.x = -Math.PI / 2 // 关键!让平面正对相机 + this.rectangle.position.set( + this.recStartPos.x, + this.recStartPos.y, + this.recStartPos.z + ) + this.viewport.scene.add(this.rectangle) + } + } + + updateRectangle(position: THREE.Vector3) { + if (!this.rectangle || !this.recStartPos) return + // console.log('updateRectangle', this.recStartPos, position) + + const width = position.x - this.recStartPos.x + const height = position.z - this.recStartPos.z + + const newWidth = Math.abs(width) + const newHeight = Math.abs(height) + + // 清理旧几何体 + this.rectangle.geometry.dispose() + this.rectangle.geometry = new THREE.PlaneGeometry(newWidth, newHeight) + this.rectangle.position.set( + this.recStartPos.x + width / 2, + this.recStartPos.y, + this.recStartPos.z + height / 2 + ) + } + + onMouseDown(event: MouseEvent) { + if (event.shiftKey) { + // 记录鼠标按下位置 + this.recStartPos = this.viewport.getClosestIntersection(event) + this.createRectangle() + } + } + + + onMouseMove(event: MouseEvent) { + if (!this.recStartPos) { + this.disposeRect() + } + // 更新矩形大小或重新生成矩形 + const position = this.viewport.getClosestIntersection(event) + if (!position) return + this.updateRectangle(position) + } + + disposeRect() { + if (this.rectangle !== null) { + this.viewport.scene.remove(this.rectangle) + this.rectangle.geometry.dispose() + this.rectangle = null + } + this.recStartPos = null + } + + onMouseUp(event: MouseEvent) { + this.disposeRect() + } +} diff --git a/src/core/engine/SceneHelp.ts b/src/core/engine/SceneHelp.ts new file mode 100644 index 0000000..fff615c --- /dev/null +++ b/src/core/engine/SceneHelp.ts @@ -0,0 +1,133 @@ +import * as THREE from 'three' +import type WorldModel from '@/core/manager/WorldModel' + +/** + * 场景帮助类 + * 封装了 Three.js 场景,并提供管理工具 + */ +export default class SceneHelp { + scene: THREE.Scene + axesHelper: THREE.GridHelper + gridHelper: THREE.GridHelper + worldModel: WorldModel + catalogCode: string + + /** + * 构造函数 + * @param worldModel 世界模型实例 + * @param catalogCode 世界目录 ID + */ + constructor(worldModel: WorldModel, catalogCode: string) { + this.worldModel = worldModel + this.catalogCode = catalogCode + + // 初始化 Three.js 场景 + this.scene = new THREE.Scene() + this.scene.background = new THREE.Color(0xeeeeee) + + // 辅助线 + const gridOption = this.worldModel.gridOption + const axesHelper = new THREE.GridHelper(gridOption.axesSize, gridOption.axesDivisions) + axesHelper.material.color.setHex(gridOption.axesColor) + axesHelper.material.linewidth = 2 + axesHelper.material.opacity = gridOption.gridOpacity + axesHelper.material.transparent = true + if (!gridOption.axesEnabled) { + axesHelper.visible = false + } + + // @ts-ignore + axesHelper.material.vertexColors = false + this.axesHelper = axesHelper + this.scene.add(this.axesHelper) + + const gridHelper = new THREE.GridHelper(gridOption.gridSize, gridOption.gridDivisions) + gridHelper.material.color.setHex(gridOption.gridColor) + gridHelper.material.opacity = gridOption.gridOpacity + gridHelper.material.transparent = true + // @ts-ignore + gridHelper.material.vertexColors = false + if (!gridOption.gridEnabled) { + gridHelper.visible = false + } + + this.gridHelper = gridHelper + this.scene.add(this.gridHelper) + + // 光照 + const ambientLight = new THREE.AmbientLight(0xffffff, 0.8) + this.scene.add(ambientLight) + + // const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5) + // directionalLight.position.set(5, 5, 5).multiplyScalar(3) + // directionalLight.castShadow = true + // scene.add(directionalLight) + // + // const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1) + // scene.add(hemisphereLight) + } + + // /** + // * 加载指定楼层的实体并添加到场景 + // * @param floorId 楼层 ID + // */ + // async loadFloorEntities(floorId: string): Promise { + // 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 + } +} \ No newline at end of file diff --git a/src/core/engine/Viewport.ts b/src/core/engine/Viewport.ts new file mode 100644 index 0000000..1b9571b --- /dev/null +++ b/src/core/engine/Viewport.ts @@ -0,0 +1,483 @@ +import _ from 'lodash' +import * as THREE from 'three' +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' +import Stats from 'three/examples/jsm/libs/stats.module' +import type WorldModel from '../manager/WorldModel' +import $ from 'jquery' +import { reactive, watch } from 'vue' +import type IControls from '../controls/IControls' +import { CSS3DRenderer } from 'three/examples/jsm/renderers/CSS3DRenderer' +import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer' +import { getAllItemTypes, type ItemTypeMeta } from '@/model/itemType/ItemTypeDefine.ts' +import type { ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine.ts' + +import SceneHelp from './SceneHelp' +import SelectInspect from '../controls/SelectInspect' +import MouseMoveInspect from '../controls/MouseMoveInspect' +import EsDragControls from '../controls/EsDragControls' +import EntityManager from '../manager/EntityManager' +import InteractionManager from '@/core/manager/InteractionManager' +import { calcPositionUseSnap } from '@/core/ModelUtils' +import StateManager from '@/core/manager/StateManager.ts' + +/** + * 编辑器对象 + * 这是非双向绑定的设计器对象,不记录状态,只记录全局使用到的对象,(实体类使用) + */ +export default class Viewport { + viewerDom: HTMLElement + camera: THREE.OrthographicCamera + renderer: THREE.WebGLRenderer + statsControls: Stats + controls: OrbitControls + raycaster: THREE.Raycaster + dragControl: any // EsDragControls + animationFrameId: any = null + scene: SceneHelp + + tools: IControls[] = [ + new MouseMoveInspect(), + new SelectInspect() + ] + + // 状态管理器 + stateManager: StateManager + + // 实体管理器 + entityManager = new EntityManager() + + // 交互管理器 + interactionManager = new InteractionManager() + + beginSync() { + } + + syncOnRemove(id: string) { + } + + syncOnUpdate(item: VDataItem) { + } + + syncOnAppend(item: VDataItem) { + } + + endSync() { + } + + get worldModel(): WorldModel { + return this.scene.worldModel + } + + get axesHelper(): THREE.GridHelper { + return this.scene.axesHelper + } + + get gridHelper(): THREE.GridHelper { + return this.scene.gridHelper + } + + /** + * 监听窗口大小变化 + */ + resizeObserver?: ResizeObserver + + /** + * vue 的 watcher + */ + watchList: (() => void)[] = [] + + css2DRenderer: CSS2DRenderer = new CSS2DRenderer() + css3DRenderer: CSS3DRenderer = new CSS3DRenderer() + + //@ts-ignore + state: ViewportState = reactive({ + isReady: false, + cursorMode: 'normal', + selectedObject: null, + camera: { + position: { x: 0, y: 0, z: 0 }, + rotation: { x: 0, y: 0, z: 0 } + }, + mouse: { + x: 0, + y: 0 + } + }) + + constructor(sceneHelp: SceneHelp, viewerDom: HTMLElement) { + this.scene = sceneHelp + this.viewerDom = viewerDom + } + + /** + * 初始化 THREE 渲染器 + */ + initThree(option: InitThreeOption) { + console.log('viewport on catelogCode: ' + this.scene.catalogCode) + const viewerDom = this.viewerDom + + // 初始化各种管理器 + this.entityManager.init(this) + this.interactionManager.init(this) + this.stateManager = new StateManager(option.stateManagerId, this) + + // 渲染器 + const renderer = new THREE.WebGLRenderer({ + logarithmicDepthBuffer: true, + antialias: true, + alpha: true, + precision: 'mediump', + premultipliedAlpha: true, + preserveDrawingBuffer: false, + powerPreference: 'high-performance' + }) + renderer.debug.checkShaderErrors = true + //@ts-ignore + renderer.outputEncoding = THREE.SRGBColorSpace + renderer.clearDepth() + renderer.shadowMap.enabled = true + renderer.toneMapping = THREE.ACESFilmicToneMapping + renderer.setPixelRatio(Math.max(Math.ceil(window.devicePixelRatio), 1)) + renderer.setViewport(0, 0, this.viewerDom.offsetWidth, this.viewerDom.offsetHeight) + renderer.setSize(this.viewerDom.offsetWidth, this.viewerDom.offsetHeight) + + viewerDom.appendChild(renderer.domElement) + + renderer.domElement.style.touchAction = 'none' + + // 防止重复添加 + if (this.css2DRenderer.domElement.parentNode !== this.viewerDom) { + this.css2DRenderer.setSize(this.viewerDom.offsetWidth, this.viewerDom.offsetHeight) + this.css2DRenderer.domElement.setAttribute('id', 'astral-3d-preview-css2DRenderer') + this.css2DRenderer.domElement.style.position = 'absolute' + this.css2DRenderer.domElement.style.top = '0px' + this.css2DRenderer.domElement.style.pointerEvents = 'none' + + this.viewerDom.appendChild(this.css2DRenderer.domElement) + } + + // 防止重复添加 + if (this.css3DRenderer.domElement.parentNode !== this.viewerDom) { + this.css3DRenderer.setSize(this.viewerDom.offsetWidth, this.viewerDom.offsetHeight) + this.css3DRenderer.domElement.setAttribute('id', 'astral-3d-preview-css3DRenderer') + this.css3DRenderer.domElement.style.position = 'absolute' + this.css3DRenderer.domElement.style.top = '0px' + this.css3DRenderer.domElement.style.pointerEvents = 'none' + + this.viewerDom.appendChild(this.css3DRenderer.domElement) + } + + this.renderer = renderer + + // 创建正交摄像机 + this.initMode2DCamera() + + // 注册拖拽组件 + this.dragControl = new EsDragControls(this) + + // 性能监控 + const statsControls = new Stats() + this.statsControls = statsControls + statsControls.showPanel(0) + statsControls.dom.style.position = 'absolute' + statsControls.dom.style.top = '0' + statsControls.dom.style.left = '0' + viewerDom.parentElement.parentElement.appendChild(statsControls.dom) + $(statsControls.dom).children().css('height', '28px') + + this.animate() + + // 监听事件 + this.watchList.push(watch(() => this.state.camera.position.y, (newVal) => { + if (!this.state.isReady) { + return + } + this.updateGridVisibility() + })) + + // 监听窗口大小变化 + if (this.resizeObserver) { + this.resizeObserver.unobserve(this.viewerDom) + } + this.resizeObserver = new ResizeObserver(this.handleResize.bind(this)) + this.resizeObserver.observe(this.viewerDom) + + // 初始化射线投射器 + this.raycaster = new THREE.Raycaster() + + // 初始化所有常驻工具 + for (const tool of this.tools) { + tool.init(this) + } + + // 触发所有物品类型的 afterAddViewport 方法 + _.forEach(getAllItemTypes(), (itemType: ItemTypeDefineOption) => { + itemType.clazz.afterAddViewport(this) + }) + + this.state.isReady = true + } + + /** + * 初始化2D相机 + */ + initMode2DCamera() { + if (this.camera) { + this.scene.remove(this.camera) + } + + // ============================ 创建正交相机 + const viewerDom = this.viewerDom + const cameraNew = new THREE.OrthographicCamera( + viewerDom.clientWidth / -2, + viewerDom.clientWidth / 2, + viewerDom.clientHeight / 2, + viewerDom.clientHeight / -2, + 1, + 500 + ) + cameraNew.position.set(0, 100, 0) + cameraNew.lookAt(0, 0, 0) + cameraNew.zoom = 30 + this.camera = cameraNew + this.scene.add(this.camera) + + // ============================ 创建控制器 + const controlsNew = new OrbitControls( + this.camera, + this.renderer.domElement + ) + controlsNew.enableDamping = false + controlsNew.enableZoom = true + controlsNew.enableRotate = false + controlsNew.mouseButtons = { LEFT: THREE.MOUSE.PAN, RIGHT: THREE.MOUSE.PAN } // 鼠标中键平移 + controlsNew.screenSpacePanning = false // 定义平移时如何平移相机的位置 控制不上下移动 + controlsNew.listenToKeyEvents(viewerDom) // 监听键盘事件 + controlsNew.keys = { LEFT: 'KeyA', UP: 'KeyW', RIGHT: 'KeyD', BOTTOM: 'KeyS' } + controlsNew.addEventListener('change', this.syncCameraState.bind(this)) + controlsNew.panSpeed = 1 + controlsNew.keyPanSpeed = 20 // normal 7 + controlsNew.minDistance = 0.1 + controlsNew.maxDistance = 1000 + this.controls = controlsNew + + this.camera.updateProjectionMatrix() + + this.syncCameraState() + } + + offset = 0 + + /** + * 动画循环 + */ + animate() { + this.animationFrameId = requestAnimationFrame(this.animate.bind(this)) + this.renderView() + + if(window['lineMaterial']) { + this.offset -= 0.002 + window['lineMaterial'].dashOffset = this.offset + } + } + + /** + * 渲染视图 + */ + renderView() { + this.statsControls?.update() + this.renderer?.render(this.scene.scene, this.camera) + + this.css2DRenderer.render(this.scene.scene, this.camera) + this.css3DRenderer.render(this.scene.scene, this.camera) + } + + /** + * 同步相机状态到全局状态 + */ + syncCameraState() { + if (this.camera) { + const camera = this.camera + + this.state.camera.position.x = camera.position.x + this.state.camera.position.y = this.getEffectiveViewDistance() + this.state.camera.position.z = camera.position.z + } + } + + /** + * 计算相机到目标的有效视距 + */ + getEffectiveViewDistance() { + if (!this.camera) { + return 10 + } + const camera = this.camera + const viewHeight = (camera.top - camera.bottom) / camera.zoom + // 假设我们希望匹配一个虚拟的透视相机(通常使用45度fov作为参考) + const referenceFOV = 45 // 参考视场角 + return viewHeight / (2 * Math.tan(THREE.MathUtils.degToRad(referenceFOV) / 2)) + } + + handleResize(entries: any) { + for (let entry of entries) { + // entry.contentRect包含了元素的尺寸信息 + // console.log('Element size changed:', entry.contentRect) + + const width = entry.contentRect.width + const height = entry.contentRect.height + + if (this.camera instanceof THREE.PerspectiveCamera) { + this.camera.aspect = width / height + this.camera.updateProjectionMatrix() + + } else if (this.camera instanceof THREE.OrthographicCamera) { + this.camera.left = width / -2 + this.camera.right = width / 2 + this.camera.top = height / 2 + this.camera.bottom = height / -2 + this.camera.updateProjectionMatrix() + } + + this.renderer.setSize(width, height) + this.css2DRenderer.setSize(width, height) + this.css3DRenderer.setSize(width, height) + break + } + } + + /** + * 根据可视化范围更新网格的透明度 + */ + updateGridVisibility() { + const cameraDistance = this.state.camera.position.y + const maxVisibleDistance = 60 // 网格完全可见的最大距离 + const fadeStartDistance = 15 // 开始淡出的距离 + + // 计算透明度(0~1) + let opacity = 0.8 + if (cameraDistance > fadeStartDistance) { + opacity = 0.8 - Math.min((cameraDistance - fadeStartDistance) / (maxVisibleDistance - fadeStartDistance) * 0.8, 0.8) + } + + // 修改网格材质透明度 + this.gridHelper.material.opacity = opacity + this.gridHelper.visible = opacity > 0 + } + + destroy() { + this.state.isReady = false + + if (this.animationFrameId !== null) { + cancelAnimationFrame(this.animationFrameId) + this.animationFrameId = null + } + + if (this.watchList) { + _.forEach(this.watchList, (unWatchFn => { + if (typeof unWatchFn === 'function') { + unWatchFn() + } + })) + this.watchList = [] + } + + if (this.tools) { + for (const tool of this.tools) { + if (tool.destory) { + tool.destory() + } + } + this.tools = [] + } + + if (this.resizeObserver) { + this.resizeObserver.unobserve(this.viewerDom) + this.resizeObserver.disconnect() + this.resizeObserver = undefined + } + + if (this.statsControls) { + this.statsControls.dom.remove() + } + + if (this.renderer) { + this.renderer.dispose() + this.renderer.forceContextLoss() + console.log('WebGL disposed, memory:', this.renderer.info.memory) + this.renderer.domElement = null + } + } + + getIntersects(point: THREE.Vector2) { + const mouse = new THREE.Vector2() + mouse.set((point.x * 2) - 1, -(point.y * 2) + 1) + this.raycaster.setFromCamera(mouse, this.camera) + + return this.raycaster.intersectObjects([this.gridHelper], false) + } + + /** + * 获取鼠标所在的 x,y,z 位置。 + * 鼠标坐标是相对于 canvas 元素 (renderer.domElement) 元素的 + */ + getClosestIntersection(e: MouseEvent) { + const _point = new THREE.Vector2() + _point.x = e.offsetX / this.renderer.domElement.offsetWidth + _point.y = e.offsetY / this.renderer.domElement.offsetHeight + + const intersects = this.getIntersects(_point) + if (intersects && intersects.length > 2) { + const point = new THREE.Vector3(intersects[0].point.x, 0.1, intersects[1].point.z) + + return calcPositionUseSnap(e, point) + } + return null + } +} + +export interface ViewportState { + /** + * 当前楼层 + */ + currentFloor: string + + /** + * 是否准备完成 + */ + isReady: boolean + + /** + * 鼠标模式 + */ + cursorMode: string // CursorMode, + + /** + * 选中的对象 + */ + selectedObject: THREE.Object3D | null + + /** + * 选中的对象的元数据 + */ + selectedObjectMeta: ItemTypeMeta | null + + /** + * 相机状态 + */ + camera: { + position: { x: number, y: number, z: number }, + rotation: { x: number, y: number, z: number } + } + + /** + * 鼠标位置(归一化坐标) + */ + mouse: { + /** + * 鼠标在设计图上的坐标 + */ + x: number, + z: number + } +} \ No newline at end of file diff --git a/src/core/manager/EntityManager.ts b/src/core/manager/EntityManager.ts new file mode 100644 index 0000000..2c8099e --- /dev/null +++ b/src/core/manager/EntityManager.ts @@ -0,0 +1,157 @@ +import * as THREE from 'three' +import { getRenderer } from './ModuleManager' +import type Viewport from '@/core/engine/Viewport.ts' + +/** + * 缓存所有实体和他们的关系, 在各个组件的渲染器会调用这个实体管理器, 进行检索 / 关系 / 获取差异等计算 + */ +export default class EntityManager { + /** + * 视窗对象, 所有状态管理器, ThreeJs场景,控制器,摄像机, 实体管理器都在这里 + */ + viewport: Viewport + + /** + * 所有数据点的实体 + */ + entities = new Map() + + /** + * 所有数据点与 THREEJS 对象的关系 + */ + objects = new Map() + + /** + * 所有关联关系 + */ + relationIndex = new Map; in: Set; out: Set }>() + + /** + * 两两关联关系与 THREEJS 对象之间的关联 + */ + lines = new Map() + + 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) + } +} \ No newline at end of file diff --git a/src/core/manager/InstancePool.ts b/src/core/manager/InstancePool.ts new file mode 100644 index 0000000..3230724 --- /dev/null +++ b/src/core/manager/InstancePool.ts @@ -0,0 +1,151 @@ +import * as THREE from 'three' + +export class InstancePool { + private mesh: THREE.InstancedMesh + private maxCount: number + private nextIndex: number = 0 + private freeIndices: number[] = [] + private matrixArray: Float32Array + private matrixTexture: THREE.DataTexture | null = null + private needsUpdate: boolean = false + private visibleCount: number = 0 + + constructor( + geometry: THREE.BufferGeometry, + material: THREE.Material | THREE.Material[], + maxCount: number + ) { + this.maxCount = maxCount + + // 创建实例化网格 + this.mesh = new THREE.InstancedMesh(geometry, material, maxCount) + this.mesh.frustumCulled = false // 禁用视锥剔除,由我们手动控制 + + // 初始化矩阵数组 + this.matrixArray = new Float32Array(maxCount * 16) + + // 初始将所有实例移到屏幕外 + this.resetAllInstances() + } + + // 获取一个可用实例 + public acquireInstance(): number | null { + let index: number + + if (this.freeIndices.length > 0) { + index = this.freeIndices.pop()! + } else if (this.nextIndex < this.maxCount) { + index = this.nextIndex++ + } else { + console.warn('Instance pool exhausted') + return null + } + + this.visibleCount++ + return index + } + + // 释放实例 + public releaseInstance(index: number): void { + if (index < 0 || index >= this.maxCount) { + console.error(`Invalid instance index: ${index}`) + return + } + + // 将实例移到屏幕外 + this.setInstanceMatrix(index, new THREE.Matrix4().setPosition(0, -10000, 0)) + this.freeIndices.push(index) + this.visibleCount-- + } + + // 设置实例的变换矩阵 + public setInstanceMatrix(index: number, matrix: THREE.Matrix4): void { + matrix.toArray(this.matrixArray, index * 16) + this.needsUpdate = true + } + + // 更新所有实例 + public update(): void { + if (!this.needsUpdate) return + + // 高效更新所有矩阵 + if (this.mesh.instanceMatrix) { + this.mesh.instanceMatrix.needsUpdate = true + } + + // 使用矩阵纹理优化(可选) + if (!this.matrixTexture) { + this.matrixTexture = new THREE.DataTexture( + this.matrixArray, + 4, // 每行4个矩阵 + this.maxCount, + THREE.RGBAFormat, + THREE.FloatType + ) + this.matrixTexture.needsUpdate = true + + // 在着色器中使用矩阵纹理 + if (Array.isArray(this.mesh.material)) { + this.mesh.material.forEach(mat => { + mat.onBeforeCompile = shader => { + this.applyMatrixTextureShader(shader) + } + }) + } else { + this.mesh.material.onBeforeCompile = shader => { + this.applyMatrixTextureShader(shader) + } + } + } else { + this.matrixTexture.needsUpdate = true + } + + this.needsUpdate = false + } + + // 获取实例化网格 + public getMesh(): THREE.InstancedMesh { + return this.mesh + } + + // 重置所有实例 + private resetAllInstances(): void { + const hiddenMatrix = new THREE.Matrix4().setPosition(0, -10000, 0) + + for (let i = 0; i < this.maxCount; i++) { + hiddenMatrix.toArray(this.matrixArray, i * 16) + this.freeIndices.push(i) + } + + this.mesh.instanceMatrix.needsUpdate = true + } + + // 应用矩阵纹理着色器修改 + private applyMatrixTextureShader(shader: THREE.WebGLProgramParametersWithUniforms): void { + shader.uniforms.instanceMatrixTexture = { value: this.matrixTexture } + + shader.vertexShader = ` + uniform sampler2D instanceMatrixTexture; + varying vec4 vInstancePosition; + + mat4 getInstanceMatrix(float index) { + vec2 texCoord = vec2(mod(index, 4.0) * 0.25, floor(index * 0.25) / ${this.maxCount.toFixed(1)}); + vec4 row1 = texture2D(instanceMatrixTexture, texCoord); + vec4 row2 = texture2D(instanceMatrixTexture, texCoord + vec2(0.25, 0.0)); + vec4 row3 = texture2D(instanceMatrixTexture, texCoord + vec2(0.5, 0.0)); + vec4 row4 = texture2D(instanceMatrixTexture, texCoord + vec2(0.75, 0.0)); + return mat4(row1, row2, row3, row4); + } + ` + shader.vertexShader + + shader.vertexShader = shader.vertexShader.replace( + '#include ', + ` + float instanceIndex = float(gl_InstanceID); + mat4 instanceMatrix = getInstanceMatrix(instanceIndex); + vec3 transformed = (instanceMatrix * vec4(position, 1.0)).xyz; + vInstancePosition = instanceMatrix * vec4(position, 1.0); + ` + ) + } +} \ No newline at end of file diff --git a/src/core/manager/InteractionManager.ts b/src/core/manager/InteractionManager.ts new file mode 100644 index 0000000..2cda0f3 --- /dev/null +++ b/src/core/manager/InteractionManager.ts @@ -0,0 +1,50 @@ +import type Viewport from '@/core/engine/Viewport.ts' +import { watch } from 'vue' +import type IControls from '@/core/controls/IControls.ts' +import type BaseInteraction from '@/core/base/BaseInteraction.ts' +import { getInteraction } from '@/core/manager/ModuleManager.ts' +import * as THREE from 'three' + +/** + * 交互管理器 + */ +export default class InteractionManager implements IControls { + private viewport: Viewport + + /** + * 当前激活的交互工具 + */ + currentTool: BaseInteraction | null = null + + //搭配 state.cursorMode = xxx 之后, currentTool.start(第一个参数) 使用 + toolStartObject: THREE.Object3D | null = null + + init(viewport: Viewport) { + this.viewport = viewport + + this.viewport.watchList.push(watch(() => this.viewport.state.cursorMode, (newVal: CursorMode) => { + const state = this.viewport.state + + if (!state.isReady) { + return + } + if (this.currentTool) { + this.currentTool.stop() + this.currentTool = null + } + if (newVal === 'normal' || !newVal) { + this.viewport.dragControl.dragControls.enabled = true + return + } + + this.currentTool = getInteraction(newVal) + this.viewport.dragControl.dragControls.enabled = false + + this.currentTool.start(this.viewport, this.toolStartObject) + this.toolStartObject = null + })) + } + + destory() { + } +} \ No newline at end of file diff --git a/src/core/manager/ModuleManager.ts b/src/core/manager/ModuleManager.ts new file mode 100644 index 0000000..cc3f523 --- /dev/null +++ b/src/core/manager/ModuleManager.ts @@ -0,0 +1,82 @@ +import * as THREE from 'three' +import BaseRenderer from '@/core/base/BaseRenderer' +import BaseInteraction from '@/core/base/BaseInteraction' +import type { IMeta } from '@/core/base/IMeta' +import BaseEntity from '@/core/base/BaseItemEntity' + +// Define the ModuleDefineOption interface +export interface ModuleDefineOption { + /** + * 物流单元类型名称 + */ + name: string; + renderer: BaseRenderer; + interaction: BaseInteraction; + meta: IMeta; + entity: new () => BaseEntity; +} + +// Internal storage for module definitions +const modules = new Map() +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(name: string): T { + const module = getModuleOption(name) + return module.renderer as T +} + +/** + * 根据物料类型名称, 获取交互控制器 + * 如果获取不了 直接抛异常 + */ +export function getInteraction(name: string): T { + const module = getModuleOption(name) + return module.interaction as T +} + +/** + * 根据物料类型名称, 获取元数据 + * 如果获取不了 直接抛异常 + */ +export function getMeta(name: string): T { + const module = getModuleOption(name) + return module.meta as T +} + +/** + * 根据物料类型名称, 获取实体类 + * 如果获取不了 直接抛异常 + */ +export function createEntity(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 +} \ No newline at end of file diff --git a/src/core/manager/StateManager.ts b/src/core/manager/StateManager.ts new file mode 100644 index 0000000..e7044c3 --- /dev/null +++ b/src/core/manager/StateManager.ts @@ -0,0 +1,562 @@ +import _ from 'lodash' +import localforage from 'localforage' +import type Viewport from '@/core/engine/Viewport.ts' +import { markRaw, reactive, ref } from 'vue' + +/** + * 一种管理 地图数据状态的管理器, 他能够对数据进行增删改查,并且能够进行撤销、重做等操作 + * 1. 管理场景数据的读取、保存, 以及临时保存、临时读取等功能 + * 2. 管理撤销、重做功能 + * 3. 各种 Interaction 交互控制组件通过如下步骤修改数据 + * - 1. 调用 beginUserWrite 开始修改数据 + * - 2. 直接修改 vdata 数据 + * - 3. 调用 endUserWrite 完成数据修改 + * 4. 内部如果进行了撤销、还原等操作,会通过 syncDataState() 方法将 vdata 数据与 viewport 进行同步 + * 5. syncDataState 方法会对比 vdata 与 viewport + * - 分别进行添加、删除、更新等操作 + * 6. 注意,如果正在读取中,需要设置 isLoading = true,外部需要等待加载完成后再进行操作 + * 状态管理器,管理内部数据结构的读取、保存、临时保存、临时读取、撤销、重做等功能 + * 当数据结构发生变化时,只对发生变化的对象进行更新 + * + * // 初始化 + * const stateManager = new StateManager('scene-1', viewport) + * + * // 加载大数据 + * await stateManager.load(largeDataSet) // 5万+ items + * + * // 修改数据 + * stateManager.beginUserWrite() + * + * // 直接修改状态(实际项目应通过封装方法) + * stateManager.vdata.items.push(newItem) + * stateManager.vdata.items[0].name = 'updated' + * stateManager.vdata.items = stateManager.vdata.items.filter(i => i.id !== 'remove-id') + * + * stateManager.endUserWrite() // 自动计算差异并保存 + * + * // 撤销操作 + * stateManager.undo() + * + * // 保存到云端 + * const data = await stateManager.save() + * + */ +export default class StateManager { + /** + * 唯一场景标识符, 用于做临时存储的 key + */ + readonly id: string + + /** + * 视口对象, 用于获取、同步当前场景的状态 + */ + readonly viewport: Viewport + + /** + * 是否发生了变化,通知外部是否需要保存数据 + */ + readonly isChanged = ref(false) + + /** + * 是否正在加载数据,通知外部是否需要等待加载完成 + */ + readonly isLoading = ref(false) + + /** + * 当前场景数据 + */ + vdata: VData + + /** + * 使用循环缓冲区存储历史记录 + */ + private historySteps: HistoryStep[] = [] + private historyIndex = -1 + private readonly maxHistorySteps = 20 + private readonly historyBufferSize: number + + // 变化追踪器 + private changeTracker: DataDiff = { + added: [], + removed: [], + updated: [] + } + + /** + * 数据快照(用于差异计算) + */ + private lastSnapshot: Map = new Map() + + // 自动保存相关 + private autoSaveInterval: number | null = null + private autoSaveIntervalMs = 5000 // 5秒自动保存 + private pendingChanges = false // 是否有待保存的更改 + private lastAutoSaveTime = 0 // 上次自动保存时间 + + /** + * @param id 唯一场景标识符, 用于做临时存储的 key + * @param viewport 视口对象, 用于获取、同步当前场景的状态 + * @param bufferSize 历史记录缓冲区大小,默认为 50 + */ + constructor(id: string, viewport: Viewport, bufferSize = 50) { + this.id = id + this.viewport = viewport + + this.historyBufferSize = bufferSize + // 初始化固定大小的历史缓冲区 + this.historySteps = new Array(this.maxHistorySteps).fill(null) + + // 启动自动保存定时器 + this.startAutoSave() + } + + /** + * 开始用户操作(创建数据快照) + */ + beginUserWrite() { + // 创建当前状态快照(非深拷贝) + this.lastSnapshot = new Map( + this.vdata.items.map(item => [item.id, _.cloneDeep(item)]) + ) + this.changeTracker = { added: [], removed: [], updated: [] } + } + + /** + * 结束用户操作(计算差异并保存) + */ + endUserWrite() { + this.calculateDiff() + this.saveStep() + this.syncDataState() + this.isChanged.value = true + this.pendingChanges = true // 标记有需要保存的更改 + } + + + /** + * 计算当前状态与快照的差异 + */ + private calculateDiff() { + const currentMap = new Map( + this.vdata.items.map(item => [item.id, item]) + ) + + // 检测删除的项目 + for (const [id] of this.lastSnapshot) { + if (!currentMap.has(id)) { + this.changeTracker.removed.push(id) + } + } + + // 检测新增和更新的项目 + for (const [id, currentItem] of currentMap) { + const lastItem = this.lastSnapshot.get(id) + + if (!lastItem) { + this.changeTracker.added.push(currentItem) + } else if (!_.isEqual(lastItem, currentItem)) { + this.changeTracker.updated.push(currentItem) + } + } + + // 清除空变更 + if (this.changeTracker.added.length === 0) delete this.changeTracker.added + if (this.changeTracker.removed.length === 0) delete this.changeTracker.removed + if (this.changeTracker.updated.length === 0) delete this.changeTracker.updated + } + + + /** + * 保存差异到历史记录 + */ + private saveStep() { + // 跳过空变更 + if ( + (!this.changeTracker.added || this.changeTracker.added.length === 0) && + (!this.changeTracker.removed || this.changeTracker.removed.length === 0) && + (!this.changeTracker.updated || this.changeTracker.updated.length === 0) + ) { + return + } + + // 使用循环缓冲区存储历史记录 + const nextIndex = (this.historyIndex + 1) % this.maxHistorySteps + this.historySteps[nextIndex] = { + diff: _.cloneDeep(this.changeTracker), + timestamp: Date.now() + } + + this.historyIndex = nextIndex + this.pendingChanges = true // 标记有需要保存的更改 + } + + + /** + * 将当前数据 与 viewport 进行同步, 对比出不同的部分,分别进行更新 + * - 调用 viewport.beginSync() 开始更新场景 + * - 调用 viewport.syncOnRemove(id) 删除场景中不存在的对象 + * - 调用 viewport.syncOnAppend(vdataItem) 添加场景中新的对象 + * - 调用 viewport.syncOnUpdate(id) 更新场景中已存在的对象 + * - 调用 viewport.endSync() 结束更新场景 + */ + syncDataState() { + // 没有变化时跳过同步 + if ( + (!this.changeTracker.added || this.changeTracker.added.length === 0) && + (!this.changeTracker.removed || this.changeTracker.removed.length === 0) && + (!this.changeTracker.updated || this.changeTracker.updated.length === 0) + ) { + return + } + + this.viewport.beginSync() + + // 处理删除 + if (this.changeTracker.removed) { + for (const id of this.changeTracker.removed) { + this.viewport.syncOnRemove(id) + } + } + + // 处理新增 + if (this.changeTracker.added) { + for (const item of this.changeTracker.added) { + this.viewport.syncOnAppend(item) + } + } + + // 处理更新 + if (this.changeTracker.updated) { + for (const item of this.changeTracker.updated) { + this.viewport.syncOnUpdate(item) + } + } + + this.viewport.endSync() + } + + + /** + * 从外部加载数据 + */ + async load(data: VData) { + this.isLoading.value = true + this.historySteps = new Array(this.maxHistorySteps).fill(null) + this.historyIndex = -1 + + try { + // 停止自动保存,避免在加载过程中触发 + this.stopAutoSave() + + // 直接替换数组引用(避免响应式开销) + this.vdata = { + id: this.id, + items: data.items, + isChanged: false, + catalog: data.catalog + } + this.fullSync() // 同步到视口 + + // 初始状态作为第一步 + this.beginUserWrite() + this.endUserWrite() + + this.isChanged.value = false + this.pendingChanges = false + + // 强制保存一次初始状态 + await this.saveToLocalstore() + this.pendingChanges = false + + console.log('[StateManager] 加载完成,共 ', data.items.length, '个对象') + + } finally { + this.isLoading.value = false + // 重新启动自动保存 + this.startAutoSave() + } + } + + /** + * 保存数据到外部 + */ + async save(): Promise { + return _.cloneDeep(this.vdata) + } + + /** + * 强制保存到本地存储 + */ + async forceSave() { + try { + await this.saveToLocalstore() + this.pendingChanges = false + return true + } catch (error) { + console.error('[StateManager] 强制保存失败:', error) + return false + } + } + + /** + * 撤销 + */ + undo() { + if (!this.undoEnabled()) return + + const step = this.historySteps[this.historyIndex] + if (!step) return + + this.applyReverseDiff(step.diff) + this.historyIndex = (this.historyIndex - 1 + this.maxHistorySteps) % this.maxHistorySteps + this.isChanged.value = true + this.pendingChanges = true + this.syncDataState() + } + + /** + * 重做 + */ + redo() { + if (!this.redoEnabled()) return + + this.historyIndex = (this.historyIndex + 1) % this.maxHistorySteps + const step = this.historySteps[this.historyIndex] + if (!step) return + + this.applyDiff(step.diff) + this.isChanged.value = true + this.pendingChanges = true + this.syncDataState() + } + + + /** + * 应用正向差异 + */ + private applyDiff(diff: DataDiff) { + // 处理删除 + if (diff.removed) { + this.vdata.items = this.vdata.items.filter(item => !diff.removed.includes(item.id)) + } + + // 处理新增 + if (diff.added) { + this.vdata.items.push(...diff.added) + } + + // 处理更新 + if (diff.updated) { + const updateMap = new Map(diff.updated.map(item => [item.id, item])) + this.vdata.items = this.vdata.items.map(item => + updateMap.has(item.id) ? updateMap.get(item.id)! : item + ) + } + + this.lastSnapshot = new Map(this.vdata.items.map(item => [item.id, _.cloneDeep(item)])) + } + + /** + * 应用反向差异(用于撤销) + */ + private applyReverseDiff(diff: DataDiff) { + // 反向处理:删除 → 添加 + if (diff.removed) { + // 从历史快照恢复被删除的项目 + const restoredItems = diff.removed + .map(id => this.lastSnapshot.get(id)) + .filter(Boolean) as VDataItem[] + + this.vdata.items.push(...restoredItems) + } + + // 反向处理:添加 → 删除 + if (diff.added) { + const addedIds = new Set(diff.added.map(item => item.id)) + this.vdata.items = this.vdata.items.filter(item => !addedIds.has(item.id)) + } + + // 反向处理:更新 → 恢复旧值 + if (diff.updated) { + const restoreMap = new Map( + diff.updated + .map(item => [item.id, this.lastSnapshot.get(item.id)]) + .filter(([, item]) => !!item) as [string, VDataItem][] + ) + + this.vdata.items = this.vdata.items.map(item => + restoreMap.has(item.id) ? restoreMap.get(item.id)! : item + ) + } + + this.lastSnapshot = new Map(this.vdata.items.map(item => [item.id, _.cloneDeep(item)])) + } + + // /** + // * 保存到本地存储(防止数据丢失) + // */ + // async saveToLocalstore() { + // // 只保存变化部分和关键元数据 + // const saveData = { + // diff: this.changeTracker, + // timestamp: Date.now(), + // itemsCount: this.vdata.items.length + // } + // + // await localforage.setItem(`scene-tmp-${this.id}`, saveData) + // } + // + // /** + // * 从本地存储加载数据 + // */ + // async loadFromLocalstore() { + // try { + // this.isLoading.value = true + // const saved: any = await localforage.getItem(`scene-tmp-${this.id}`) + // if (saved && saved.diff) { + // this.applyDiff(saved.diff) + // this.isChanged.value = true + // this.pendingChanges = true + // console.log('[StateManager] 从本地存储恢复 ', saved.itemsCount, '个对象') + // } + // + // } catch (error) { + // console.error('[StateManager] 从本地存储加载失败:', error) + // + // } finally { + // this.isLoading.value = false + // } + // } + + /** + * 保存到本地存储 浏览器indexDb(防止数据丢失) + */ + async saveToLocalstore() { + await localforage.setItem(`scene-tmp-${this.id}`, this.vdata) + } + + /** + * 从本地存储还原数据 + */ + async loadFromLocalstore() { + try { + this.isLoading.value = true + const saved: VData = await localforage.getItem(`scene-tmp-${this.id}`) + if (saved) { + this.vdata.items = saved.items || [] + this.isChanged.value = saved.isChanged || false + this.pendingChanges = true + this.fullSync() // 同步到视口 + console.log('[StateManager] 从本地存储恢复', this.vdata.items.length, '个对象') + } + + } catch (error) { + console.error('[StateManager] 从本地存储加载失败:', error) + } finally { + this.isLoading.value = false + } + } + + private fullSync() { + this.viewport.beginSync() + this.vdata.items.forEach(item => { + this.viewport.syncOnAppend(item) + }) + this.viewport.endSync() + } + + undoEnabled() { + return this.historyIndex >= 0 && this.historySteps[this.historyIndex] + } + + redoEnabled() { + const nextIndex = (this.historyIndex + 1) % this.maxHistorySteps + return !!this.historySteps[nextIndex] + } + + /** + * 删除本地存储 + */ + async removeLocalstore() { + try { + await localforage.removeItem(`scene-tmp-${this.id}`) + console.log('[StateManager] 本地存储已清除') + } catch (error) { + console.error('[StateManager] 清除本地存储失败:', error) + } + } + + /** + * 启动自动保存定时器 + */ + startAutoSave() { + // if (this.autoSaveInterval) return + // + // this.autoSaveInterval = window.setInterval(() => { + // this.autoSaveIfNeeded() + // }, this.autoSaveIntervalMs) + } + + /** + * 停止自动保存定时器 + */ + stopAutoSave() { + // if (this.autoSaveInterval) { + // clearInterval(this.autoSaveInterval) + // this.autoSaveInterval = null + // } + } + + /** + * 检查是否需要自动保存并执行 + */ + private async autoSaveIfNeeded() { + // 没有变化或正在加载时跳过 + if (!this.pendingChanges || this.isLoading.value) return + + try { + await this.saveToLocalstore() + this.pendingChanges = false + this.lastAutoSaveTime = Date.now() + console.debug(`[StateManager] 自动保存成功 at ${new Date().toLocaleTimeString()}`) + } catch (error) { + console.error('[StateManager] 自动保存失败:', error) + } + } + + /** + * 销毁资源 + */ + destroy() { + this.stopAutoSave() + // 清理引用 + delete this.vdata + delete this.historySteps + } + + /** + * 获取自动保存状态 + */ + getAutoSaveStatus() { + return { + enabled: this.autoSaveInterval !== null, + interval: this.autoSaveIntervalMs, + lastSaveTime: this.lastAutoSaveTime, + pendingChanges: this.pendingChanges + } + } +} + + +// 差异类型定义 +interface DataDiff { + added: VDataItem[] + removed: string[] + updated: VDataItem[] +} + +// 历史记录项 +interface HistoryStep { + diff: DataDiff + timestamp: number +} \ No newline at end of file diff --git a/src/core/manager/WorldModel.ts b/src/core/manager/WorldModel.ts new file mode 100644 index 0000000..e2d0510 --- /dev/null +++ b/src/core/manager/WorldModel.ts @@ -0,0 +1,180 @@ +import _ from 'lodash' +import { reactive, watch } from 'vue' +import EventBus from '@/runtime/EventBus' + +export interface WorldModelState { + isOpened: boolean // 是否已打开世界模型 + catalog: Catalog // 世界模型目录数据 + + catalogCode: string // 当前楼层的目录代码 + stateManagerId: string // 当前楼层的状态管理器id +} + +/** + * 世界模型 + */ +export default class WorldModel { + data: any = null + + /** + * 世界模型双向绑定的状态数据 + */ + state: WorldModelState = reactive({ + isOpened: false, // 是否已打开世界模型 + catalogCode: '', + stateManagerId: '', // 当前楼层的状态管理器id + catalog: [] as Catalog // 世界模型目录数据 + }) + + get gridOption(): IGridHelper { + const data = _.get(this.data, 'Tool.gridHelper') + return _.defaultsDeep(data, { + axesEnabled: true, + axesSize: 1000, + axesDivisions: 4, + axesColor: 0x000000, + axesOpacity: 1, + + gridEnabled: true, // 启用网格 + gridSize: 1000, // 网格大小, 单位米 + gridDivisions: 1000, // 网格分割数 + gridColor: 0x999999, // 网格颜色, 十六进制颜色值 + gridOpacity: 0.8, // 网格透明度 + snapEnabled: true, // 启用吸附 + snapDistance: 0.25 // 吸附距离, 单位米 + }) + } + + constructor() { + } + + init() { + // 观察 this.state.catalogCode 的变化, 如果变化就调用 catalogCodeChange 方法 + watch(() => this.state.catalogCode, (newValue, oldValue) => { + worldModel.loadFloor(newValue) + }) + + return Promise.all([ + import('@/modules/measure') + + ]).then(() => { + console.log('世界模型初始化完成') + }) + } + + /** + * 读取世界地图数据目录 + */ + loadCatalog(data: any) { + this.data = data + this.state.catalog = data.catalog + this.state.isOpened = true + } + + /** + * 加载指定目录的楼层数据, 并返回世界模型+楼层的唯一id + */ + loadFloor(catalogCode: string) { + if (!catalogCode) { + this.state.catalogCode = '' + this.state.stateManagerId = '' + EventBus.dispatch('catalogChanged', { + catalogCode: this.state.catalogCode, + stateManagerId: this.state.stateManagerId + }) + return + } + + const floor = _.find(this.data.items, r => r.catalogCode === catalogCode && r.t === 'floor') + if (!floor) { + system.msg('楼层不存在: ' + catalogCode) + return + } + + this.state.catalogCode = catalogCode + this.state.stateManagerId = this.data.project_uuid + '_' + catalogCode + EventBus.dispatch('catalogChanged', { + catalogCode: this.state.catalogCode, + stateManagerId: this.state.stateManagerId + }) + } + + // loadFloorToScene(viewport: Viewport, scene: THREE.Scene, levelCode: string) { + // let floor = _.find(this.data.items, r => r.name === levelCode && r.t === 'floor') + // if (!floor) { + // console.info(`新建楼层: ${levelCode}`) + // + // if (!_.isArray(this.data.items)) { + // this.data.items = [] + // } + // floor = { name: levelCode, t: 'floor', items: [] } + // this.data.items.push(floor) + // } + // + // loadSceneFromJson(viewport, scene, floor.items) + // } + + // open() { + // if (this.sceneMap.size > 0) { + // // 释放旧场景 + // this.sceneMap.forEach((scene: Scene) => { + // this.sceneDispose(scene) + // }) + // } + // if (this.viewPorts.length > 0) { + // // 注销视口 + // this.viewPorts.forEach((viewport: Viewport) => { + // this.unregisterViewport(viewport) + // }) + // } + // + // system.msg('打开世界地图完成') + // this.data = markRaw(Example1) + // this.state.openFileName = 'example1' + // this.state.allLevels = reactive(this.data.allLevels) + // } + + // /** + // * 获取当前楼层的场景, 如果没有则创建一个新的场景 + // */ + // getSceneByFloor(viewport: Viewport, floor: string) { + // if (this.sceneMap.has(floor)) { + // return this.sceneMap.get(floor) + // } else { + // const scene = this.createScene(viewport, floor) + // + // this.sceneMap.set(floor, scene) + // return scene + // } + // } + // + // /** + // * 创建一个新的场景 + // */ + // createScene(viewport: Viewport, floor: string) { + // const scene = new Scene() + // scene.background = new THREE.Color(0xeeeeee) + // + // this.loadFloorToScene(viewport, scene, floor) + // return scene + // } + + // /** + // * 注册视口 + // */ + // registerViewport(viewport: Viewport) { + // this.viewPorts = this.viewPorts || [] + // this.viewPorts.push(viewport) + // } + // + // /** + // * 注销视口 + // */ + // unregisterViewport(viewport: Viewport) { + // const index = this.viewPorts.indexOf(viewport) + // if (index > -1) { + // this.viewPorts.splice(index, 1) + // } + // } + +} \ No newline at end of file diff --git a/src/designer/Constract.ts b/src/designer/Constract.ts deleted file mode 100644 index afd4a7e..0000000 --- a/src/designer/Constract.ts +++ /dev/null @@ -1,20 +0,0 @@ -export default Object.freeze({ - // 光标相关 - CursorModeNormal: 'normal', - - // 关联相关 - CursorModeALink: 'ALink', - CursorModeSLink: 'SLink', - CursorModePointCallback: 'PointCallback', - CursorModePointAdd: 'PointAdd', - CursorModeLinkAdd: 'LinkAdd', - CursorModeLinkAdd2: 'LinkAdd2', - - // 测量相关的光标模式 - CursorModeMeasure: 'measure', - - CursorModeConveyor: 'conveyor', - - // 选择模式 - CursorModeSelectByRec: 'selectByRec' -}) \ No newline at end of file diff --git a/src/designer/ModelView.vue b/src/designer/ModelView.vue deleted file mode 100644 index 78e4ba9..0000000 --- a/src/designer/ModelView.vue +++ /dev/null @@ -1,8 +0,0 @@ - - \ No newline at end of file diff --git a/src/designer/StateManager.ts b/src/designer/StateManager.ts deleted file mode 100644 index 91f3eeb..0000000 --- a/src/designer/StateManager.ts +++ /dev/null @@ -1,554 +0,0 @@ -import _ from 'lodash' -import localforage from 'localforage' -import type Viewport from '@/designer/Viewport.ts' -import { markRaw, reactive, ref } from 'vue' -import type { VData, VDataItem } from '@/types/Types' - -/** - * THREE.js 场景状态管理器. - * 从数据结构还原 threejs 画布的一种结构 - * 1. 管理场景数据的读取、保存, 以及临时保存、临时读取等功能 - * 2. 管理撤销、重做功能 - * 3. 用户侧会通过如下步骤修改数据 - * - 1. 调用 beginUserWrite 开始修改数据 - * - 2. 直接修改 vdata 数据 - * - 3. 调用 endUserWrite 完成数据修改 - * 4. 内部如果进行了撤销、还原等操作,会通过 syncDataState() 方法将 vdata 数据与 viewport 进行同步 - * 5. syncDataState 方法会对比 vdata 与 viewport - * - 分别进行添加、删除、更新等操作 - * 6. 注意,如果正在读取中,需要设置 isLoading = true,外部需要等待加载完成后再进行操作 - * 状态管理器,管理内部数据结构的读取、保存、临时保存、临时读取、撤销、重做等功能 - * 当数据结构发生变化时,只对发生变化的对象进行更新 - * - * // 初始化 - * const stateManager = new StateManager('scene-1', viewport) - * - * // 加载大数据 - * await stateManager.load(largeDataSet) // 5万+ items - * - * // 修改数据 - * stateManager.beginUserWrite() - * - * // 直接修改状态(实际项目应通过封装方法) - * stateManager.vdata.items.push(newItem) - * stateManager.vdata.items[0].name = 'updated' - * stateManager.vdata.items = stateManager.vdata.items.filter(i => i.id !== 'remove-id') - * - * stateManager.endUserWrite() // 自动计算差异并保存 - * - * // 撤销操作 - * stateManager.undo() - * - * // 保存到云端 - * const data = await stateManager.save() - * - */ -export default class StateManager { - /** - * 是否发生了变化,通知外部是否需要保存数据 - */ - isChanged = ref(false) - - /** - * 是否正在加载数据,通知外部是否需要等待加载完成 - */ - isLoading = ref(false) - - /** - * 当前场景数据 - */ - vdata: VData = { items: [], isChanged: false } - - /** - * 唯一场景标识符, 用于做临时存储的 key - */ - id: string - - /** - * 视口对象, 用于获取、同步当前场景的状态 - */ - viewport: Viewport - - /** - * 使用循环缓冲区存储历史记录 - */ - private historySteps: HistoryStep[] = [] - private historyIndex = -1 - private readonly maxHistorySteps = 20 - private readonly historyBufferSize: number - - // 变化追踪器 - private changeTracker: DataDiff = { - added: [], - removed: [], - updated: [] - } - - /** - * 数据快照(用于差异计算) - */ - private lastSnapshot: Map = new Map() - - // 自动保存相关 - private autoSaveInterval: number | null = null - private autoSaveIntervalMs = 5000 // 5秒自动保存 - private pendingChanges = false // 是否有待保存的更改 - private lastAutoSaveTime = 0 // 上次自动保存时间 - - /** - * @param id 唯一场景标识符, 用于做临时存储的 key - * @param viewport 视口对象, 用于获取、同步当前场景的状态 - * @param bufferSize 历史记录缓冲区大小,默认为 50 - */ - constructor(id: string, viewport: Viewport, bufferSize = 50) { - this.id = id - this.viewport = markRaw(viewport) - this.historyBufferSize = bufferSize - // 初始化固定大小的历史缓冲区 - this.historySteps = new Array(this.maxHistorySteps).fill(null) - - // 启动自动保存定时器 - this.startAutoSave() - } - - /** - * 开始用户操作(创建数据快照) - */ - beginUserWrite() { - // 创建当前状态快照(非深拷贝) - this.lastSnapshot = new Map( - this.vdata.items.map(item => [item.id, _.cloneDeep(item)]) - ) - this.changeTracker = { added: [], removed: [], updated: [] } - } - - /** - * 结束用户操作(计算差异并保存) - */ - endUserWrite() { - this.calculateDiff() - this.saveStep() - this.syncDataState() - this.isChanged.value = true - this.pendingChanges = true // 标记有需要保存的更改 - } - - - /** - * 计算当前状态与快照的差异 - */ - private calculateDiff() { - const currentMap = new Map( - this.vdata.items.map(item => [item.id, item]) - ) - - // 检测删除的项目 - for (const [id] of this.lastSnapshot) { - if (!currentMap.has(id)) { - this.changeTracker.removed.push(id) - } - } - - // 检测新增和更新的项目 - for (const [id, currentItem] of currentMap) { - const lastItem = this.lastSnapshot.get(id) - - if (!lastItem) { - this.changeTracker.added.push(currentItem) - } else if (!_.isEqual(lastItem, currentItem)) { - this.changeTracker.updated.push(currentItem) - } - } - - // 清除空变更 - if (this.changeTracker.added.length === 0) delete this.changeTracker.added - if (this.changeTracker.removed.length === 0) delete this.changeTracker.removed - if (this.changeTracker.updated.length === 0) delete this.changeTracker.updated - } - - - /** - * 保存差异到历史记录 - */ - private saveStep() { - // 跳过空变更 - if ( - (!this.changeTracker.added || this.changeTracker.added.length === 0) && - (!this.changeTracker.removed || this.changeTracker.removed.length === 0) && - (!this.changeTracker.updated || this.changeTracker.updated.length === 0) - ) { - return - } - - // 使用循环缓冲区存储历史记录 - const nextIndex = (this.historyIndex + 1) % this.maxHistorySteps - this.historySteps[nextIndex] = { - diff: _.cloneDeep(this.changeTracker), - timestamp: Date.now() - } - - this.historyIndex = nextIndex - this.pendingChanges = true // 标记有需要保存的更改 - } - - - /** - * 将当前数据 与 viewport 进行同步, 对比出不同的部分,分别进行更新 - * - 调用 viewport.beginSync() 开始更新场景 - * - 调用 viewport.syncOnRemove(id) 删除场景中不存在的对象 - * - 调用 viewport.syncOnAppend(vdataItem) 添加场景中新的对象 - * - 调用 viewport.syncOnUpdate(id) 更新场景中已存在的对象 - * - 调用 viewport.endSync() 结束更新场景 - */ - syncDataState() { - // 没有变化时跳过同步 - if ( - (!this.changeTracker.added || this.changeTracker.added.length === 0) && - (!this.changeTracker.removed || this.changeTracker.removed.length === 0) && - (!this.changeTracker.updated || this.changeTracker.updated.length === 0) - ) { - return - } - - this.viewport.beginSync() - - // 处理删除 - if (this.changeTracker.removed) { - for (const id of this.changeTracker.removed) { - this.viewport.syncOnRemove(id) - } - } - - // 处理新增 - if (this.changeTracker.added) { - for (const item of this.changeTracker.added) { - this.viewport.syncOnAppend(item) - } - } - - // 处理更新 - if (this.changeTracker.updated) { - for (const item of this.changeTracker.updated) { - this.viewport.syncOnUpdate(item) - } - } - - this.viewport.endSync() - } - - - /** - * 从外部加载数据 - */ - async load(items: VDataItem[]) { - this.isLoading.value = true - this.historySteps = new Array(this.maxHistorySteps).fill(null) - this.historyIndex = -1 - - try { - // 停止自动保存,避免在加载过程中触发 - this.stopAutoSave() - - // 直接替换数组引用(避免响应式开销) - this.vdata.items = items - this.fullSync() // 同步到视口 - - // 初始状态作为第一步 - this.beginUserWrite() - this.endUserWrite() - - this.isChanged.value = false - this.pendingChanges = false - - // 强制保存一次初始状态 - await this.saveToLocalstore() - this.pendingChanges = false - - console.log('[StateManager] 加载完成,共 ', items.length, '个对象') - - } finally { - this.isLoading.value = false - // 重新启动自动保存 - this.startAutoSave() - } - } - - /** - * 保存数据到外部 - */ - async save(): Promise { - return _.cloneDeep(this.vdata) - } - - /** - * 强制保存到本地存储 - */ - async forceSave() { - try { - await this.saveToLocalstore() - this.pendingChanges = false - return true - } catch (error) { - console.error('[StateManager] 强制保存失败:', error) - return false - } - } - - /** - * 撤销 - */ - undo() { - if (!this.undoEnabled()) return - - const step = this.historySteps[this.historyIndex] - if (!step) return - - this.applyReverseDiff(step.diff) - this.historyIndex = (this.historyIndex - 1 + this.maxHistorySteps) % this.maxHistorySteps - this.isChanged.value = true - this.pendingChanges = true - this.syncDataState() - } - - /** - * 重做 - */ - redo() { - if (!this.redoEnabled()) return - - this.historyIndex = (this.historyIndex + 1) % this.maxHistorySteps - const step = this.historySteps[this.historyIndex] - if (!step) return - - this.applyDiff(step.diff) - this.isChanged.value = true - this.pendingChanges = true - this.syncDataState() - } - - - /** - * 应用正向差异 - */ - private applyDiff(diff: DataDiff) { - // 处理删除 - if (diff.removed) { - this.vdata.items = this.vdata.items.filter(item => !diff.removed.includes(item.id)) - } - - // 处理新增 - if (diff.added) { - this.vdata.items.push(...diff.added) - } - - // 处理更新 - if (diff.updated) { - const updateMap = new Map(diff.updated.map(item => [item.id, item])) - this.vdata.items = this.vdata.items.map(item => - updateMap.has(item.id) ? updateMap.get(item.id)! : item - ) - } - - this.lastSnapshot = new Map(this.vdata.items.map(item => [item.id, _.cloneDeep(item)])) - } - - /** - * 应用反向差异(用于撤销) - */ - private applyReverseDiff(diff: DataDiff) { - // 反向处理:删除 → 添加 - if (diff.removed) { - // 从历史快照恢复被删除的项目 - const restoredItems = diff.removed - .map(id => this.lastSnapshot.get(id)) - .filter(Boolean) as VDataItem[] - - this.vdata.items.push(...restoredItems) - } - - // 反向处理:添加 → 删除 - if (diff.added) { - const addedIds = new Set(diff.added.map(item => item.id)) - this.vdata.items = this.vdata.items.filter(item => !addedIds.has(item.id)) - } - - // 反向处理:更新 → 恢复旧值 - if (diff.updated) { - const restoreMap = new Map( - diff.updated - .map(item => [item.id, this.lastSnapshot.get(item.id)]) - .filter(([, item]) => !!item) as [string, VDataItem][] - ) - - this.vdata.items = this.vdata.items.map(item => - restoreMap.has(item.id) ? restoreMap.get(item.id)! : item - ) - } - - this.lastSnapshot = new Map(this.vdata.items.map(item => [item.id, _.cloneDeep(item)])) - } - - // /** - // * 保存到本地存储(防止数据丢失) - // */ - // async saveToLocalstore() { - // // 只保存变化部分和关键元数据 - // const saveData = { - // diff: this.changeTracker, - // timestamp: Date.now(), - // itemsCount: this.vdata.items.length - // } - // - // await localforage.setItem(`scene-tmp-${this.id}`, saveData) - // } - // - // /** - // * 从本地存储加载数据 - // */ - // async loadFromLocalstore() { - // try { - // this.isLoading.value = true - // const saved: any = await localforage.getItem(`scene-tmp-${this.id}`) - // if (saved && saved.diff) { - // this.applyDiff(saved.diff) - // this.isChanged.value = true - // this.pendingChanges = true - // console.log('[StateManager] 从本地存储恢复 ', saved.itemsCount, '个对象') - // } - // - // } catch (error) { - // console.error('[StateManager] 从本地存储加载失败:', error) - // - // } finally { - // this.isLoading.value = false - // } - // } - - async saveToLocalstore() { - await localforage.setItem(`scene-tmp-${this.id}`, this.vdata) - } - - async loadFromLocalstore() { - try { - this.isLoading.value = true - const saved: VData = await localforage.getItem(`scene-tmp-${this.id}`) - if (saved) { - this.vdata.items = saved.items || [] - this.isChanged.value = saved.isChanged || false - this.pendingChanges = true - this.fullSync() // 同步到视口 - console.log('[StateManager] 从本地存储恢复', this.vdata.items.length, '个对象') - } - - } catch (error) { - console.error('[StateManager] 从本地存储加载失败:', error) - } finally { - this.isLoading.value = false - } - } - - private fullSync() { - this.viewport.beginSync() - this.vdata.items.forEach(item => { - this.viewport.syncOnAppend(item) - }) - this.viewport.endSync() - } - - undoEnabled() { - return this.historyIndex >= 0 && this.historySteps[this.historyIndex] - } - - redoEnabled() { - const nextIndex = (this.historyIndex + 1) % this.maxHistorySteps - return !!this.historySteps[nextIndex] - } - - /** - * 删除本地存储 - */ - async removeLocalstore() { - try { - await localforage.removeItem(`scene-tmp-${this.id}`) - console.log('[StateManager] 本地存储已清除') - } catch (error) { - console.error('[StateManager] 清除本地存储失败:', error) - } - } - - /** - * 启动自动保存定时器 - */ - startAutoSave() { - if (this.autoSaveInterval) return - - this.autoSaveInterval = window.setInterval(() => { - this.autoSaveIfNeeded() - }, this.autoSaveIntervalMs) - } - - /** - * 停止自动保存定时器 - */ - stopAutoSave() { - if (this.autoSaveInterval) { - clearInterval(this.autoSaveInterval) - this.autoSaveInterval = null - } - } - - /** - * 检查是否需要自动保存并执行 - */ - private async autoSaveIfNeeded() { - // 没有变化或正在加载时跳过 - if (!this.pendingChanges || this.isLoading.value) return - - try { - await this.saveToLocalstore() - this.pendingChanges = false - this.lastAutoSaveTime = Date.now() - console.debug(`[StateManager] 自动保存成功 at ${new Date().toLocaleTimeString()}`) - } catch (error) { - console.error('[StateManager] 自动保存失败:', error) - } - } - - /** - * 销毁资源 - */ - destroy() { - this.stopAutoSave() - this.removeLocalstore().catch(console.error) - // 清理引用 - this.viewport = null as any - this.vdata.items = [] - this.historySteps = [] - } - - /** - * 获取自动保存状态 - */ - getAutoSaveStatus() { - return { - enabled: this.autoSaveInterval !== null, - interval: this.autoSaveIntervalMs, - lastSaveTime: this.lastAutoSaveTime, - pendingChanges: this.pendingChanges - } - } -} - - -// 差异类型定义 -interface DataDiff { - added: VDataItem[] - removed: string[] - updated: VDataItem[] -} - -// 历史记录项 -interface HistoryStep { - diff: DataDiff - timestamp: number -} \ No newline at end of file diff --git a/src/designer/Viewport.ts b/src/designer/Viewport.ts deleted file mode 100644 index c38e0bd..0000000 --- a/src/designer/Viewport.ts +++ /dev/null @@ -1,540 +0,0 @@ -import _ from 'lodash' -import * as THREE from 'three' -import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' -import EsDragControls from './model2DEditor/EsDragControls' -import Stats from 'three/examples/jsm/libs/stats.module' -import type WorldModel from '@/model/WorldModel.ts' -import $ from 'jquery' -import { reactive, watch } from 'vue' -import type { ITool } from '@/designer/model2DEditor/tools/ITool.ts' -import { CSS3DRenderer } from 'three/examples/jsm/renderers/CSS3DRenderer' -import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer' -import { getAllItemTypes, type ItemTypeMeta } from '@/model/itemType/ItemTypeDefine.ts' -import type { ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine.ts' - -import type Toolbox from '@/model/itemType/Toolbox.ts' -import { calcPositionUseSnap } from '@/model/ModelUtils.ts' -import SelectInspect from '@/designer/model2DEditor/tools/SelectInspect.ts' -import MouseMoveInspect from '@/designer/model2DEditor/tools/MouseMoveInspect.ts' -import type { CursorMode, VDataItem } from '@/types/Types' - -/** - * 编辑器对象 - * 这是非双向绑定的设计器对象,不记录状态,只记录全局使用到的对象,(实体类使用) - */ -export default class Viewport { - viewerDom: HTMLElement - scene: THREE.Scene - camera: THREE.OrthographicCamera - renderer: THREE.WebGLRenderer - axesHelper: THREE.GridHelper - gridHelper: THREE.GridHelper - statsControls: Stats - controls: OrbitControls - worldModel: WorldModel - raycaster: THREE.Raycaster - dragControl: any // EsDragControls - animationFrameId: any = null - - //搭配 state.cursorMode = xxx 之后, currentTool.start(第一个参数) 使用 - toolStartObject: THREE.Object3D | null = null - currentTool: Toolbox | null = null - tools: ITool[] = [ - new MouseMoveInspect(), - new SelectInspect() - ] - toolbox: Record = {} - - objectMap: Map = new Map() - - beginSync() { - } - - syncOnRemove(id: string) { - } - - syncOnUpdate(item: VDataItem) { - } - - syncOnAppend(item: VDataItem) { - } - - endSync() { - } - - /** - * 监听窗口大小变化 - */ - resizeObserver?: ResizeObserver - - /** - * vue 的 watcher - */ - watchList: (() => void)[] = [] - - css2DRenderer: CSS2DRenderer = new CSS2DRenderer() - css3DRenderer: CSS3DRenderer = new CSS3DRenderer() - - //@ts-ignore - state: ViewportState = reactive({ - currentFloor: '', - isReady: false, - cursorMode: 'normal', - selectedObject: null, - camera: { - position: { x: 0, y: 0, z: 0 }, - rotation: { x: 0, y: 0, z: 0 } - }, - mouse: { - x: 0, - y: 0 - } - }) - - constructor(worldModel: WorldModel) { - this.worldModel = worldModel - } - - /** - * 初始化 THREE 渲染器 - */ - initThree(viewerDom: HTMLElement, floor: string) { - console.log('viewport on floor', floor) - this.state.currentFloor = floor - this.viewerDom = viewerDom - const rect = viewerDom.getBoundingClientRect() - this.worldModel.registerViewport(this) - - // 场景 - const scene = this.worldModel.getSceneByFloor(this, this.state.currentFloor) - this.scene = scene - - // 渲染器 - const renderer = new THREE.WebGLRenderer({ - logarithmicDepthBuffer: true, - antialias: true, - alpha: true, - precision: 'mediump', - premultipliedAlpha: true, - preserveDrawingBuffer: false, - powerPreference: 'high-performance' - }) - renderer.debug.checkShaderErrors = true - //@ts-ignore - renderer.outputEncoding = THREE.SRGBColorSpace - renderer.clearDepth() - renderer.shadowMap.enabled = true - renderer.toneMapping = THREE.ACESFilmicToneMapping - renderer.setPixelRatio(Math.max(Math.ceil(window.devicePixelRatio), 1)) - renderer.setViewport(0, 0, this.viewerDom.offsetWidth, this.viewerDom.offsetHeight) - renderer.setSize(this.viewerDom.offsetWidth, this.viewerDom.offsetHeight) - - viewerDom.appendChild(renderer.domElement) - - renderer.domElement.style.touchAction = 'none' - - // 防止重复添加 - if (this.css2DRenderer.domElement.parentNode !== this.viewerDom) { - this.css2DRenderer.setSize(this.viewerDom.offsetWidth, this.viewerDom.offsetHeight) - this.css2DRenderer.domElement.setAttribute('id', 'astral-3d-preview-css2DRenderer') - this.css2DRenderer.domElement.style.position = 'absolute' - this.css2DRenderer.domElement.style.top = '0px' - this.css2DRenderer.domElement.style.pointerEvents = 'none' - - this.viewerDom.appendChild(this.css2DRenderer.domElement) - } - - // 防止重复添加 - if (this.css3DRenderer.domElement.parentNode !== this.viewerDom) { - this.css3DRenderer.setSize(this.viewerDom.offsetWidth, this.viewerDom.offsetHeight) - this.css3DRenderer.domElement.setAttribute('id', 'astral-3d-preview-css3DRenderer') - this.css3DRenderer.domElement.style.position = 'absolute' - this.css3DRenderer.domElement.style.top = '0px' - this.css3DRenderer.domElement.style.pointerEvents = 'none' - - this.viewerDom.appendChild(this.css3DRenderer.domElement) - } - - this.renderer = renderer - - // 创建正交摄像机 - this.initMode2DCamera() - - // 注册拖拽组件 - this.dragControl = new EsDragControls(this) - - // 辅助线 - const gridOption = this.worldModel.gridOption - const axesHelper = new THREE.GridHelper(gridOption.axesSize, gridOption.axesDivisions) - axesHelper.material.color.setHex(gridOption.axesColor) - axesHelper.material.linewidth = 2 - axesHelper.material.opacity = gridOption.gridOpacity - axesHelper.material.transparent = true - if (!gridOption.axesEnabled) { - axesHelper.visible = false - } - - // @ts-ignore - axesHelper.material.vertexColors = false - this.axesHelper = axesHelper - this.scene.add(this.axesHelper) - - const gridHelper = new THREE.GridHelper(gridOption.gridSize, gridOption.gridDivisions) - gridHelper.material.color.setHex(gridOption.gridColor) - gridHelper.material.opacity = gridOption.gridOpacity - gridHelper.material.transparent = true - // @ts-ignore - gridHelper.material.vertexColors = false - if (!gridOption.gridEnabled) { - gridHelper.visible = false - } - - this.gridHelper = gridHelper - this.scene.add(this.gridHelper) - - // 光照 - const ambientLight = new THREE.AmbientLight(0xffffff, 0.8) - scene.add(ambientLight) - - // const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5) - // directionalLight.position.set(5, 5, 5).multiplyScalar(3) - // directionalLight.castShadow = true - // scene.add(directionalLight) - // - // const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1) - // scene.add(hemisphereLight) - - // 性能监控 - const statsControls = new Stats() - this.statsControls = statsControls - statsControls.showPanel(0) - statsControls.dom.style.position = 'absolute' - statsControls.dom.style.top = '0' - statsControls.dom.style.left = '0' - viewerDom.parentElement.parentElement.appendChild(statsControls.dom) - $(statsControls.dom).children().css('height', '28px') - - this.animate() - - // 监听事件 - this.watchList.push(watch(() => this.state.camera.position.y, (newVal) => { - if (!this.state.isReady) { - return - } - this.updateGridVisibility() - })) - this.watchList.push(watch(() => this.state.cursorMode, (newVal: CursorMode) => { - if (!this.state.isReady) { - return - } - if (this.currentTool) { - this.currentTool.stop() - this.currentTool = null - } - if (newVal === 'normal' || !newVal) { - this.dragControl.dragControls.enabled = true - return - } - - const currentTool = this.toolbox[newVal] - if (currentTool) { - // 选择标尺工具 - this.currentTool = currentTool - this.dragControl.dragControls.enabled = false - - } else { - system.showErrorDialog(`当前鼠标模式 ${newVal} 不支持`) - } - - if (this.currentTool) { - this.currentTool.start(this.toolStartObject) - this.toolStartObject = null - } - })) - - // 监听窗口大小变化 - if (this.resizeObserver) { - this.resizeObserver.unobserve(this.viewerDom) - } - this.resizeObserver = new ResizeObserver(this.handleResize.bind(this)) - this.resizeObserver.observe(this.viewerDom) - - // 初始化射线投射器 - this.raycaster = new THREE.Raycaster() - - // 初始化所有常驻工具 - for (const tool of this.tools) { - tool.init(this) - } - - // 触发所有物品类型的 afterAddViewport 方法 - _.forEach(getAllItemTypes(), (itemType: ItemTypeDefineOption) => { - itemType.clazz.afterAddViewport(this) - }) - - this.state.isReady = true - } - - /** - * 初始化2D相机 - */ - initMode2DCamera() { - if (this.camera) { - this.scene.remove(this.camera) - } - - // ============================ 创建正交相机 - const viewerDom = this.viewerDom - const cameraNew = new THREE.OrthographicCamera( - viewerDom.clientWidth / -2, - viewerDom.clientWidth / 2, - viewerDom.clientHeight / 2, - viewerDom.clientHeight / -2, - 1, - 500 - ) - cameraNew.position.set(0, 100, 0) - cameraNew.lookAt(0, 0, 0) - cameraNew.zoom = 30 - this.camera = cameraNew - this.scene.add(this.camera) - - // ============================ 创建控制器 - const controlsNew = new OrbitControls( - this.camera, - this.renderer.domElement - ) - controlsNew.enableDamping = false - controlsNew.enableZoom = true - controlsNew.enableRotate = false - controlsNew.mouseButtons = { LEFT: THREE.MOUSE.PAN, RIGHT: THREE.MOUSE.PAN } // 鼠标中键平移 - controlsNew.screenSpacePanning = false // 定义平移时如何平移相机的位置 控制不上下移动 - controlsNew.listenToKeyEvents(viewerDom) // 监听键盘事件 - controlsNew.keys = { LEFT: 'KeyA', UP: 'KeyW', RIGHT: 'KeyD', BOTTOM: 'KeyS' } - controlsNew.addEventListener('change', this.syncCameraState.bind(this)) - controlsNew.panSpeed = 1 - controlsNew.keyPanSpeed = 20 // normal 7 - controlsNew.minDistance = 0.1 - controlsNew.maxDistance = 1000 - this.controls = controlsNew - - this.camera.updateProjectionMatrix() - - this.syncCameraState() - } - - offset = 0 - - /** - * 动画循环 - */ - animate() { - this.animationFrameId = requestAnimationFrame(this.animate.bind(this)) - this.renderView() - - this.offset -= 0.002 - window['lineMaterial'].dashOffset = this.offset - } - - /** - * 渲染视图 - */ - renderView() { - this.statsControls?.update() - this.renderer?.render(this.scene, this.camera) - - this.css2DRenderer.render(this.scene, this.camera) - this.css3DRenderer.render(this.scene, this.camera) - } - - /** - * 同步相机状态到全局状态 - */ - syncCameraState() { - if (this.camera) { - const camera = this.camera - - this.state.camera.position.x = camera.position.x - this.state.camera.position.y = this.getEffectiveViewDistance() - this.state.camera.position.z = camera.position.z - } - } - - /** - * 计算相机到目标的有效视距 - */ - getEffectiveViewDistance() { - if (!this.camera) { - return 10 - } - const camera = this.camera - const viewHeight = (camera.top - camera.bottom) / camera.zoom - // 假设我们希望匹配一个虚拟的透视相机(通常使用45度fov作为参考) - const referenceFOV = 45 // 参考视场角 - return viewHeight / (2 * Math.tan(THREE.MathUtils.degToRad(referenceFOV) / 2)) - } - - handleResize(entries: any) { - for (let entry of entries) { - // entry.contentRect包含了元素的尺寸信息 - console.log('Element size changed:', entry.contentRect) - - const width = entry.contentRect.width - const height = entry.contentRect.height - - if (this.camera instanceof THREE.PerspectiveCamera) { - this.camera.aspect = width / height - this.camera.updateProjectionMatrix() - - } else if (this.camera instanceof THREE.OrthographicCamera) { - this.camera.left = width / -2 - this.camera.right = width / 2 - this.camera.top = height / 2 - this.camera.bottom = height / -2 - this.camera.updateProjectionMatrix() - } - - this.renderer.setSize(width, height) - this.css2DRenderer.setSize(width, height) - this.css3DRenderer.setSize(width, height) - break - } - } - - /** - * 根据可视化范围更新网格的透明度 - */ - updateGridVisibility() { - const cameraDistance = this.state.camera.position.y - const maxVisibleDistance = 60 // 网格完全可见的最大距离 - const fadeStartDistance = 15 // 开始淡出的距离 - - // 计算透明度(0~1) - let opacity = 0.8 - if (cameraDistance > fadeStartDistance) { - opacity = 0.8 - Math.min((cameraDistance - fadeStartDistance) / (maxVisibleDistance - fadeStartDistance) * 0.8, 0.8) - } - - // 修改网格材质透明度 - this.gridHelper.material.opacity = opacity - this.gridHelper.visible = opacity > 0 - } - - destroy() { - this.state.isReady = false - - if (this.animationFrameId !== null) { - cancelAnimationFrame(this.animationFrameId) - this.animationFrameId = null - } - - if (this.watchList) { - _.forEach(this.watchList, (unWatchFn => { - if (typeof unWatchFn === 'function') { - unWatchFn() - } - })) - this.watchList = [] - } - - if (this.tools) { - for (const tool of this.tools) { - if (tool.destory) { - tool.destory() - } - } - this.tools = [] - } - - if (this.resizeObserver) { - this.resizeObserver.unobserve(this.viewerDom) - this.resizeObserver.disconnect() - this.resizeObserver = undefined - } - - this.worldModel.unregisterViewport(this) - - if (this.statsControls) { - this.statsControls.dom.remove() - } - - if (this.renderer) { - this.renderer.dispose() - this.renderer.forceContextLoss() - console.log('WebGL disposed, memory:', this.renderer.info.memory) - this.renderer.domElement = null - } - } - - getIntersects(point: THREE.Vector2) { - const mouse = new THREE.Vector2() - mouse.set((point.x * 2) - 1, -(point.y * 2) + 1) - this.raycaster.setFromCamera(mouse, this.camera) - - return this.raycaster.intersectObjects([this.gridHelper], false) - } - - /** - * 获取鼠标所在的 x,y,z 位置。 - * 鼠标坐标是相对于 canvas 元素 (renderer.domElement) 元素的 - */ - getClosestIntersection(e: MouseEvent) { - const _point = new THREE.Vector2() - _point.x = e.offsetX / this.renderer.domElement.offsetWidth - _point.y = e.offsetY / this.renderer.domElement.offsetHeight - - const intersects = this.getIntersects(_point) - if (intersects && intersects.length > 2) { - const point = new THREE.Vector3(intersects[0].point.x, 0.1, intersects[1].point.z) - - return calcPositionUseSnap(e, point) - } - return null - } -} - -export interface ViewportState { - /** - * 当前楼层 - */ - currentFloor: string - - /** - * 是否准备完成 - */ - isReady: boolean - - /** - * 鼠标模式 - */ - cursorMode: string // CursorMode, - - /** - * 选中的对象 - */ - selectedObject: THREE.Object3D | null - - /** - * 选中的对象的元数据 - */ - selectedObjectMeta: ItemTypeMeta | null - - /** - * 相机状态 - */ - camera: { - position: { x: number, y: number, z: number }, - rotation: { x: number, y: number, z: number } - } - - /** - * 鼠标位置(归一化坐标) - */ - mouse: { - /** - * 鼠标在设计图上的坐标 - */ - x: number, - z: number - } -} \ No newline at end of file diff --git a/src/designer/menus/EditMenu.ts b/src/designer/menus/EditMenu.ts deleted file mode 100644 index d8e84f0..0000000 --- a/src/designer/menus/EditMenu.ts +++ /dev/null @@ -1,111 +0,0 @@ -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' - -export default defineMenu((menus) => { - menus.insertChildren('modelFile', - { - name: 'modelFile', - label: '编辑', - icon: renderIcon('ModelFile'), - order: 1 - }, - [ - { - name: 'find', label: '全局查找', icon: SvgCode.find, order: 1, tip: 'Ctrl+H', - click: () => { - system.msg('全局查找') - } - }, - { - name: 'resource', label: '资源定位', icon: SvgCode.find, order: 1.1, tip: 'Ctrl+Shift+R', divided: true, - click: () => { - system.msg('资源定位') - } - }, - { - name: 'undo', label: '撤销', icon: SvgCode.undo, order: 2, tip: 'Ctrl+Z', disabled: true, - click: () => { - system.msg('撤销') - } - }, - { - name: 'redo', label: '重做', icon: SvgCode.redo, order: 3, tip: 'Ctrl+Y', divided: true, - click() { - system.msg('重做') - } - }, - { - name: 'copy', label: '复制', icon: SvgCode.copy, order: 4, tip: 'Ctrl+C', - click() { - system.msg('复制') - } - }, - { - name: 'cut', label: '剪切', icon: SvgCode.cut, order: 5, tip: 'Ctrl+X', - click() { - system.msg('剪切') - } - }, - { - name: 'paste', label: '粘贴', icon: SvgCode.paste, order: 6, tip: 'Ctrl+V', - click() { - system.msg('粘贴') - } - }, - { - name: 'delete', label: '删除', icon: SvgCode.delete, order: 7, tip: 'key-delete', divided: true, - click() { - deletePointByKeyboard() - } - }, - { - name: 'edit_property', label: '快速转换', order: 8, - children: [ - { - name: 'edit_property_esc', label: '取消', order: 1, tip: 'key-esc', - click() { - escByKeyboard() - } - }, - { - name: 'edit_property_rotate', label: '转向90度', order: 1, tip: 'key-r', - click() { - system.msg('转向90度') - } - }, - { - name: 'edit_append', label: '快速添加', tip: 'key-q', - click() { - quickCopyByMouse() - } - }, - { - name: 'edit_up', label: '上移', tip: 'key-up', - click() { - system.msg('↑') - } - }, - { - name: 'edit_down', label: '下移', tip: 'key-down', - click() { - system.msg('↓') - } - }, - { - name: 'edit_left', label: '左移', tip: 'key-left', - click() { - system.msg('←') - } - }, - { - name: 'edit_right', label: '右移', tip: 'key-right', - click() { - system.msg('→') - } - } - ] - } - ]) -}) \ No newline at end of file diff --git a/src/designer/menus/FileMenu.ts b/src/designer/menus/FileMenu.ts deleted file mode 100644 index 239ab3c..0000000 --- a/src/designer/menus/FileMenu.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { renderIcon } from '@/utils/webutils.ts' -import { defineMenu } from '@/runtime/DefineMenu.ts' -import SvgCode from '@/components/icons/SvgCode' -import { Open } from '@element-plus/icons-vue' - -export default defineMenu((menus) => { - menus.insertChildren('file', - { - name: 'file', label: '模型', icon: renderIcon('ModelFile'), order: 1, disabled: false - }, - [ - { - name: 'open', label: '打开', icon: SvgCode.open, order: 1, tip: 'Ctrl+O', - click: () => { - system.msg('打开模型文件') - } - }, - { - name: 'save', label: '保存', icon: SvgCode.save, order: 2, tip: 'Ctrl+S', - click: () => { - system.msg('保存模型文件') - } - }, - { - name: 'saveAs', label: '另存为', icon: renderIcon('ModelFile'), order: 3, - click: () => { - system.msg('另存为模型文件') - } - } - ] - ) -}) \ No newline at end of file diff --git a/src/designer/menus/Model3DView.ts b/src/designer/menus/Model3DView.ts deleted file mode 100644 index 8de6eca..0000000 --- a/src/designer/menus/Model3DView.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { defineMenu } from '@/runtime/DefineMenu.ts' -import Model3DView from '@/designer/model3DView/Model3DView.vue' - -export default defineMenu((menus) => { - menus.insertChildren('tool', - { - name: 'tool', label: '小工具', order: 3, disabled: false - }, - [ - { - name: 'model3dview', label: '模型查看器', order: 1, - click: () => { - system.showDialog(Model3DView, { - title: '模型查看器', - width: 950, - height: 400, - showClose: true, - showMax: true, - showCancelButton: false, - showOkButton: false, - dialogClass: 'model-3d-view-wrap' - }) - } - } - ] - ) -}) \ No newline at end of file diff --git a/src/designer/menus/Tools.ts b/src/designer/menus/Tools.ts deleted file mode 100644 index 23fc9f5..0000000 --- a/src/designer/menus/Tools.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineMenu } from '@/runtime/DefineMenu.ts' -import { renderIcon } from '@/utils/webutils.ts' - -export default defineMenu((menus) => { - menus.insertChildren('file', - { - name: 'tool', label: '小工具', order: 3, disabled: false - }, - [] - ) -}) \ No newline at end of file diff --git a/src/designer/metaComponents/ColorItem.vue b/src/designer/metaComponents/ColorItem.vue deleted file mode 100644 index 1423a26..0000000 --- a/src/designer/metaComponents/ColorItem.vue +++ /dev/null @@ -1,34 +0,0 @@ - - \ No newline at end of file diff --git a/src/designer/metaComponents/IMetaProp.ts b/src/designer/metaComponents/IMetaProp.ts deleted file mode 100644 index fec9e8d..0000000 --- a/src/designer/metaComponents/IMetaProp.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as THREE from 'three' -import { type ItemTypeMetaItem } from '@/model/itemType/ItemTypeDefine.ts' -import { defineComponent, type PropType } from 'vue' -import type Viewport from '@/designer/Viewport.ts' -import EventBus from '@/runtime/EventBus' - -export default defineComponent({ - props: { - prop: Object as PropType, - viewport: Object as PropType - }, - mounted() { - EventBus.$on('objectChanged', (data) => { - //@ts-ignore - if (typeof this.refreshValue === 'function') { - //@ts-ignore - this.refreshValue() - } - }) - - this.$nextTick(() => { - //@ts-ignore - if (typeof this.refreshValue === 'function') { - //@ts-ignore - this.refreshValue() - } - }) - }, - computed: { - object3D(): THREE.Object3D { - return this.viewport.state.selectedObject - } - } -}) \ No newline at end of file diff --git a/src/designer/metaComponents/NumberInput.vue b/src/designer/metaComponents/NumberInput.vue deleted file mode 100644 index 62a5dac..0000000 --- a/src/designer/metaComponents/NumberInput.vue +++ /dev/null @@ -1,33 +0,0 @@ - - \ No newline at end of file diff --git a/src/designer/metaComponents/SwitchItem.vue b/src/designer/metaComponents/SwitchItem.vue deleted file mode 100644 index 7fad9e1..0000000 --- a/src/designer/metaComponents/SwitchItem.vue +++ /dev/null @@ -1,33 +0,0 @@ - - \ No newline at end of file diff --git a/src/designer/metaComponents/TextInput.vue b/src/designer/metaComponents/TextInput.vue deleted file mode 100644 index b67a1cc..0000000 --- a/src/designer/metaComponents/TextInput.vue +++ /dev/null @@ -1,33 +0,0 @@ - - \ No newline at end of file diff --git a/src/designer/metaComponents/Transform.vue b/src/designer/metaComponents/Transform.vue deleted file mode 100644 index e2f9404..0000000 --- a/src/designer/metaComponents/Transform.vue +++ /dev/null @@ -1,170 +0,0 @@ - - - \ No newline at end of file diff --git a/src/designer/metaComponents/UUIDItem.vue b/src/designer/metaComponents/UUIDItem.vue deleted file mode 100644 index 841f55d..0000000 --- a/src/designer/metaComponents/UUIDItem.vue +++ /dev/null @@ -1,33 +0,0 @@ - - \ No newline at end of file diff --git a/src/designer/model2DEditor/DragControls.js b/src/designer/model2DEditor/DragControls.js deleted file mode 100644 index e8fe318..0000000 --- a/src/designer/model2DEditor/DragControls.js +++ /dev/null @@ -1,209 +0,0 @@ -import { - EventDispatcher, - Matrix4, - Plane, - Raycaster, - Vector2, - Vector3 -} from 'three' -import { calcPositionUseSnap } from '@/model/ModelUtils.js' - -const _plane = new Plane() -const _raycaster = new Raycaster() - -const _pointer = new Vector2() -const _offset = new Vector3() -const _intersection = new Vector3() -const _worldPosition = new Vector3() -const _inverseMatrix = new Matrix4() - -class DragControls extends EventDispatcher { - constructor(_objects, _camera, _domElement) { - super() - - _domElement.style.touchAction = 'none' // disable touch scroll - - let _selected = null, _hovered = null - - const _intersections = [] - - // - - let isMove = false - let isMouseDownClicked = false - const scope = this - - function activate() { - _domElement.addEventListener('pointermove', onPointerMove) - _domElement.addEventListener('pointerdown', onPointerDown) - _domElement.addEventListener('pointerup', onPointerCancel) - _domElement.addEventListener('pointerleave', onPointerCancel) - } - - function deactivate() { - _domElement.removeEventListener('pointermove', onPointerMove) - _domElement.removeEventListener('pointerdown', onPointerDown) - _domElement.removeEventListener('pointerup', onPointerCancel) - _domElement.removeEventListener('pointerleave', onPointerCancel) - - _domElement.style.cursor = '' - } - - function dispose() { - deactivate() - } - - function setObjects(objects) { - _objects = objects - } - - function getObjects() { - return _objects - } - - function getRaycaster() { - return _raycaster - } - - function onPointerMove(event) { - if (!scope.enabled || !scope.enabledMove) return - - if (isMouseDownClicked) { - _domElement.style.cursor = 'move' - } - - isMove = true - updatePointer(event) - _raycaster.setFromCamera(_pointer, _camera) - - if (_selected) { - if (_raycaster.ray.intersectPlane(_plane, _intersection)) { - const pos = _intersection.sub(_offset).applyMatrix4(_inverseMatrix) - const newIntersection = calcPositionUseSnap(event, pos) - _selected.position.copy(newIntersection) - } - scope.dispatchEvent({ type: 'drag', object: _selected }) - return - } - - // hover support - if (event.pointerType === 'mouse' || event.pointerType === 'pen') { - - _intersections.length = 0 - - _raycaster.setFromCamera(_pointer, _camera) - _raycaster.intersectObjects(_objects, true, _intersections) - - if (_intersections.length > 0) { - - const object = _intersections[0].object - - _plane.setFromNormalAndCoplanarPoint(_camera.getWorldDirection(_plane.normal), _worldPosition.setFromMatrixPosition(object.matrixWorld)) - - if (_hovered !== object && _hovered !== null) { - - scope.dispatchEvent({ type: 'hoveroff', object: _hovered }) - - _domElement.style.cursor = 'auto' - _hovered = null - - } - - if (_hovered !== object) { - - scope.dispatchEvent({ type: 'hoveron', object: object }) - - _domElement.style.cursor = 'pointer' - _hovered = object - - } - - } else { - - if (_hovered !== null) { - scope.dispatchEvent({ type: 'hoveroff', object: _hovered }) - _domElement.style.cursor = 'auto' - _hovered = null - } - - } - - } - - } - - function onPointerDown(event) { - if (scope.enabled === false) return - - updatePointer(event) - - _intersections.length = 0 - - _raycaster.setFromCamera(_pointer, _camera) - let objects = _objects - - _raycaster.intersectObjects(objects, true, _intersections) - - if (_intersections.length > 0) { - _selected = (scope.transformGroup === true) ? _objects[0] : _intersections[0].object - - if (scope.enabledMove) { - _plane.setFromNormalAndCoplanarPoint(_camera.getWorldDirection(_plane.normal), _worldPosition.setFromMatrixPosition(_selected.matrixWorld)) - if (_raycaster.ray.intersectPlane(_plane, _intersection)) { - _inverseMatrix.copy(_selected.parent.matrixWorld).invert() - _offset.copy(_intersection).sub(_worldPosition.setFromMatrixPosition(_selected.matrixWorld)) - } - - // setTimeout(() => { - // _domElement.style.cursor = 'move' - // }, 20) - isMouseDownClicked = true - } - scope.dispatchEvent({ type: 'dragstart', object: _selected, e: event }) - } - - isMove = false - } - - function onPointerCancel(event) { - if (scope.enabled === false) return - - if (_selected) { - scope.dispatchEvent({ type: 'dragend', object: _selected, e: event }) - _selected = null - } else if (!isMove) { - // 添加点击空白处的事件 - scope.dispatchEvent({ type: 'clickblank', e: event }) - } - - _domElement.style.cursor = _hovered ? 'pointer' : 'auto' - isMouseDownClicked = false - } - - function updatePointer(event) { - const rect = _domElement.getBoundingClientRect() - - _pointer.x = (event.clientX - rect.left) / rect.width * 2 - 1 - _pointer.y = -(event.clientY - rect.top) / rect.height * 2 + 1 - } - - activate() - - // API - - this.enabled = true - this.enabledMove = true - this.transformGroup = false - - this.activate = activate - this.deactivate = deactivate - this.dispose = dispose - this.setObjects = setObjects - this.getObjects = getObjects - this.getRaycaster = getRaycaster - - } - -} - -export { DragControls } diff --git a/src/designer/model2DEditor/EsDragControls.ts b/src/designer/model2DEditor/EsDragControls.ts deleted file mode 100644 index 9b129c0..0000000 --- a/src/designer/model2DEditor/EsDragControls.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { DragControls } from './DragControls.js' -import * as THREE from 'three' -import type Viewport from '@/designer/Viewport.ts' -import Constract from '@/designer/Constract.ts' -import { getItemTypeByName } from '@/model/itemType/ItemTypeDefine.ts' -import type { ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine.ts' -import { markRaw } from 'vue' -import EventBus from '@/runtime/EventBus' - -// dragControls 绑定函数 -let dragStartFn, dragFn, dragEndFn, clickblankFn - -export default class EsDragControls { - _dragObjects: THREE.Object3D[] = [] // 拖拽对象 - dragControls: any - private onDownPosition: { x: number; y: number } = { x: -1, y: -1 } - - viewport: Viewport - isDragging = false - - constructor(viewport) { - this.viewport = viewport - - // 物体拖拽控制器 - this.dragControls = new DragControls(this._dragObjects, viewport.camera, viewport.renderer.domElement) - this.dragControls.deactivate() // 默认禁用 - dragStartFn = this.dragControlsStart.bind(this) - this.dragControls.addEventListener('dragstart', dragStartFn) - dragFn = this.drag.bind(this) - this.dragControls.addEventListener('drag', dragFn) - dragEndFn = this.dragControlsEnd.bind(this) - this.dragControls.addEventListener('dragend', dragEndFn) - // 点击可拖拽物体之外 - clickblankFn = this.clickblank.bind(this) - this.dragControls.addEventListener('clickblank', clickblankFn) - } - - set domElement(element: HTMLElement) { - this.dragControls.setDomElement(element) - } - - setDragObjects(objects: THREE.Object3D[], type: 'eq' | 'push' | 'remove' = 'eq') { - // 当前拖拽对象为空时加入对象需激活控制器 - if (this._dragObjects.length === 0) { - if (objects.length > 0) { - this.dragControls.activate() - } - - this._dragObjects = objects - } else { - // 当前拖拽对象不为空时 - if (type === 'eq') { - // 是清空拖拽对象的设置,则禁用控制器 - if (objects.length === 0) { - this.dragControls.deactivate() - } - - this._dragObjects = objects - } else if (type === 'push') { - this._dragObjects.push(...objects) - } else if (type === 'remove') { - this._dragObjects = this._dragObjects.filter((item) => !objects.includes(item)) - } - } - - this.dragControls.setObjects(this._dragObjects) - } - - // 拖拽开始 - dragControlsStart(e) { - // 右键拖拽不响应 - if (e.e.button === 2 || !e.object.userData.type || !e.object.visible) return - - e.e.preventDefault() - - // 拖拽时禁用其他控制器 - this.viewport.controls.enabled = false - - this.isDragging = true - - // 记录拖拽按下的位置和对象 - this.onDownPosition = { x: e.e.clientX, y: e.e.clientY } - - if (e.object.userData?.type) { - const itemType: ItemTypeDefineOption = getItemTypeByName(e.object.userData.type) - if (itemType?.clazz) { - itemType.clazz.dragPointStart(this.viewport, e.object) - } - } - // switch (e.object.userData.type) { - // case Constract.MeasureMarker: - // this.viewport.measure.dragPointStart(e.object) - // break - // } - } - - // 拖拽中 - drag(e) { - EventBus.$emit('objectChanged', { - viewport: this, - object: e.object - }) - } - - // 拖拽结束 - dragControlsEnd(e) { - // 右键拖拽不响应 - if (e.e.button === 2 || !e.object.visible) return - - // 拖拽结束启用其他控制器 - this.viewport.controls.enabled = true - - this.isDragging = false - - if (!e.object.userData.type) return - - // 判断位置是否有变化,没有变化则为点击 - if (this.onDownPosition.x === e.e.clientX && this.onDownPosition.y === e.e.clientY) { - if (e.object.userData.onClick) { - e.object.userData.onClick(e) - } - if (e.object.userData.selectable) { - this.viewport.state.selectedObject = markRaw(e.object) - EventBus.$emit('objectChanged', { - viewport: this, - object: e.object - }) - } - } - - if (e.object.userData?.type) { - const itemType: ItemTypeDefineOption = getItemTypeByName(e.object.userData.type) - if (itemType?.clazz) { - itemType.clazz.dragPointComplete(this.viewport) - } - } - // switch (e.object.userData.type) { - // case Constract.MeasureMarker: - // this.viewport.measure.dragPointComplete() - // break - // } - } - - // 点击可拖拽物体之外 - clickblank(e) { - if (e.e.button === 2) return - } - - dispose() { - this._dragObjects = [] - - this.dragControls.removeEventListener('dragstart', dragStartFn) - this.dragControls.removeEventListener('dragend', dragEndFn) - this.dragControls.dispose() - } -} \ No newline at end of file diff --git a/src/designer/model2DEditor/Model2DEditor.vue b/src/designer/model2DEditor/Model2DEditor.vue deleted file mode 100644 index 8c6354c..0000000 --- a/src/designer/model2DEditor/Model2DEditor.vue +++ /dev/null @@ -1,73 +0,0 @@ - - \ No newline at end of file diff --git a/src/designer/model2DEditor/Model2DEditorJs.js b/src/designer/model2DEditor/Model2DEditorJs.js deleted file mode 100644 index 134de3e..0000000 --- a/src/designer/model2DEditor/Model2DEditorJs.js +++ /dev/null @@ -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 - } - } -}) diff --git a/src/designer/model2DEditor/tools/ITool.ts b/src/designer/model2DEditor/tools/ITool.ts deleted file mode 100644 index 20806e0..0000000 --- a/src/designer/model2DEditor/tools/ITool.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface ITool { - init(viewport: any): void - - destory(): void - - animate?: () => void; -} \ No newline at end of file diff --git a/src/designer/model2DEditor/tools/MouseMoveInspect.ts b/src/designer/model2DEditor/tools/MouseMoveInspect.ts deleted file mode 100644 index fc539be..0000000 --- a/src/designer/model2DEditor/tools/MouseMoveInspect.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type Viewport from '@/designer/Viewport.ts' -import type { ITool } from '@/designer/model2DEditor/tools/ITool.ts' -import * as THREE from 'three' - -let pmFn, otFn, lvFn - -/** - * 鼠标移动时,将鼠标位置的坐标转换为设计图上的坐标,并设置到 designer.mousePos 属性中 - */ -export default class MouseMoveInspect implements ITool { - viewport: Viewport - canvas: HTMLCanvasElement - - constructor() { - } - - init(viewport: Viewport) { - this.viewport = viewport - this.canvas = this.viewport.renderer.domElement as HTMLCanvasElement - - pmFn = this.mouseMove.bind(this) - otFn = this.mouseLv.bind(this) - lvFn = this.mouseLv.bind(this) - this.canvas.addEventListener('pointermove', pmFn) - this.canvas.addEventListener('pointerout', otFn) - this.canvas.addEventListener('mouseleave', lvFn) - } - - destory() { - this.canvas.removeEventListener('pointermove', pmFn) - pmFn = undefined - this.canvas.removeEventListener('pointerout', otFn) - otFn = undefined - this.canvas.removeEventListener('mouseleave', lvFn) - lvFn = undefined - } - - mouseLv() { - this.viewport.state.mouse.x = 0 - this.viewport.state.mouse.z = 0 - window['CurrentMouseInfo'] = { - x: 0, - z: 0 - } - } - - mouseMove = _.throttle(function(this: MouseMoveInspect, event: MouseEvent) { - - const pointv = new THREE.Vector2() - pointv.x = event.offsetX / this.viewport.renderer.domElement.offsetWidth - pointv.y = event.offsetY / this.viewport.renderer.domElement.offsetHeight - - const mouse = new THREE.Vector2() - mouse.set((pointv.x * 2) - 1, -(pointv.y * 2) + 1) - - // 当前鼠标所在的点 - const point = this.viewport.getClosestIntersection(event) - if (!point) { - return - } - - this.viewport.state.mouse.x = point.x - this.viewport.state.mouse.z = point.z - - window['CurrentMouseInfo'] = { - viewport: this.viewport, - x: point.x, - z: point.z, - mouse: mouse - } - - }, 1) - - animate(): void { - } -} \ No newline at end of file diff --git a/src/designer/model2DEditor/tools/SelectInspect.ts b/src/designer/model2DEditor/tools/SelectInspect.ts deleted file mode 100644 index e115b49..0000000 --- a/src/designer/model2DEditor/tools/SelectInspect.ts +++ /dev/null @@ -1,213 +0,0 @@ -import * as THREE from 'three' -import type { ITool } from './ITool.ts' -import { watch } from 'vue' -import type Viewport from '@/designer/Viewport.ts' -import { Line2 } from 'three/examples/jsm/lines/Line2.js' -import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js' -import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js' -import { getItemTypeByName } from '@/model/itemType/ItemTypeDefine.ts' -import EventBus from '@/runtime/EventBus' - -let pdFn, pmFn, puFn - -/** - * 选择工具,用于在设计器中显示选中对象的包围盒 - */ -export default class SelectInspect implements ITool { - viewport: Viewport - /** - * 线框材质,用于显示选中对象的包围盒 - */ - material: LineMaterial = new LineMaterial({ color: 0xffff00, linewidth: 2 }) - - /** - * 矩形材质,用于显示鼠标拖拽选择的矩形区域 - */ - rectMaterial: THREE.MeshBasicMaterial = new THREE.MeshBasicMaterial({ - color: 0x000000, - opacity: 0.3, - transparent: true - }) - - /** - * 当前选中对象的矩形选择框 - */ - rectangle: THREE.Mesh | null = null - - /** - * 当前选中对象的包围盒线框 - */ - selectionBox: Line2 - - /** - * 当前鼠标所在的画布, 对应 viewport.renderer.domElement - */ - canvas: HTMLCanvasElement - - /** - * 鼠标按下时记录的起始位置,用于绘制矩形选择框 - */ - recStartPos: THREE.Vector3 | null - - constructor() { - } - - init(viewport: Viewport) { - this.viewport = viewport - this.canvas = this.viewport.renderer.domElement as HTMLCanvasElement - - pdFn = this.onMouseDown.bind(this) - this.canvas.addEventListener('pointerdown', pdFn) - pmFn = this.onMouseMove.bind(this) - this.canvas.addEventListener('pointermove', pmFn) - puFn = this.onMouseUp.bind(this) - this.canvas.addEventListener('pointerup', puFn) - - this.viewport.watchList.push(watch(() => this.viewport.state.selectedObject, this.updateSelectionBox.bind(this))) - EventBus.$on('objectChanged', (data) => { - this.updateSelectionBox(this.viewport.state.selectedObject) - }) - } - - /** - * 更新选中对象的包围盒线框 - */ - updateSelectionBox(selectedObject: THREE.Object3D) { - this.disposeSelectionBox() - this.viewport.state.selectedObjectMeta = null // 清除之前的元数据 - - if (selectedObject?.userData?.type) { - const type = selectedObject.userData.type - const itemTypeDefine = getItemTypeByName(type) - if (itemTypeDefine) { - this.viewport.state.selectedObjectMeta = itemTypeDefine.getMeta(selectedObject) - - const expandAmount = 0.2 // 扩展包围盒的大小 - // 避免某些蒙皮网格的帧延迟效应(e.g. Michelle.glb) - selectedObject.updateWorldMatrix(false, true) - - const box = new THREE.Box3().setFromObject(selectedObject) - box.expandByScalar(expandAmount) - - const size = new THREE.Vector3() - box.getSize(size) - - const center = new THREE.Vector3() - box.getCenter(center) - - // 创建包围盒几何体 - const helperGeometry = new THREE.BoxGeometry(size.x, size.y, size.z) - const edgesGeometry = new THREE.EdgesGeometry(helperGeometry) - - // 使用 LineGeometry 包装 edgesGeometry - const lineGeom = new LineGeometry() - //@ts-ignore - lineGeom.setPositions(edgesGeometry.attributes.position.array) - - const selectionBox = new Line2(lineGeom, this.material) - selectionBox.computeLineDistances() - selectionBox.position.copy(center) - selectionBox.name = 'selectionBox' - this.selectionBox = selectionBox - - this.viewport.scene.add(selectionBox) - - } - } - } - - destory() { - - this.canvas.removeEventListener('pointerdown', pdFn) - pdFn = undefined - this.canvas.removeEventListener('pointermove', pmFn) - pmFn = undefined - this.canvas.removeEventListener('pointerup', puFn) - puFn = undefined - - // 销毁选择工具 - this.disposeSelectionBox() - this.disposeRect() - } - - disposeSelectionBox() { - if (this.selectionBox) { - this.viewport.scene.remove(this.selectionBox) - this.selectionBox.geometry.dispose() - this.selectionBox = null - } - } - - createRectangle() { - if (this.rectangle !== null) { - this.disposeRect() - } - if (this.recStartPos) { - // 创建矩形 - this.rectangle = new THREE.Mesh( - new THREE.PlaneGeometry(1, 1), - this.rectMaterial - ) - this.rectangle.name = 'selectRectangle' - this.rectangle.rotation.x = -Math.PI / 2 // 关键!让平面正对相机 - this.rectangle.position.set( - this.recStartPos.x, - this.recStartPos.y, - this.recStartPos.z - ) - this.viewport.scene.add(this.rectangle) - } - } - - updateRectangle(position: THREE.Vector3) { - if (!this.rectangle || !this.recStartPos) return - // console.log('updateRectangle', this.recStartPos, position) - - const width = position.x - this.recStartPos.x - const height = position.z - this.recStartPos.z - - const newWidth = Math.abs(width) - const newHeight = Math.abs(height) - - // 清理旧几何体 - this.rectangle.geometry.dispose() - this.rectangle.geometry = new THREE.PlaneGeometry(newWidth, newHeight) - this.rectangle.position.set( - this.recStartPos.x + width / 2, - this.recStartPos.y, - this.recStartPos.z + height / 2 - ) - } - - onMouseDown(event: MouseEvent) { - if (event.shiftKey) { - // 记录鼠标按下位置 - this.recStartPos = this.viewport.getClosestIntersection(event) - this.createRectangle() - } - } - - - onMouseMove(event: MouseEvent) { - if (!this.recStartPos) { - this.disposeRect() - } - // 更新矩形大小或重新生成矩形 - const position = this.viewport.getClosestIntersection(event) - if (!position) return - this.updateRectangle(position) - } - - disposeRect() { - if (this.rectangle !== null) { - this.viewport.scene.remove(this.rectangle) - this.rectangle.geometry.dispose() - this.rectangle = null - } - this.recStartPos = null - } - - onMouseUp(event: MouseEvent) { - this.disposeRect() - } -} diff --git a/src/designer/model3DView/Model3DView.vue b/src/designer/model3DView/Model3DView.vue deleted file mode 100644 index 4cd7960..0000000 --- a/src/designer/model3DView/Model3DView.vue +++ /dev/null @@ -1,921 +0,0 @@ - - - \ No newline at end of file diff --git a/src/designer/viewWidgets/IWidgets.ts b/src/designer/viewWidgets/IWidgets.ts deleted file mode 100644 index 015f77a..0000000 --- a/src/designer/viewWidgets/IWidgets.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { defineComponent } from 'vue' -import { renderIcon } from '@/utils/webutils.js' -import Viewport, { type ViewportState } from '@/designer/Viewport.ts' - -export type IWidgetData = { - /** - * 是否激活 - */ - isActivated: boolean -} - -export default defineComponent({ - activated() { - this.isActivated = true - console.log('activated', this.$.type.name) - }, - deactivated() { - this.isActivated = false - }, - props: { - viewport: Viewport - }, - computed: { - state(): ViewportState { - return this.viewport?.state - } - }, - emits: ['close'], - data() { - return { - isActivated: false - } as IWidgetData - }, - methods: { - renderIcon, - closeMe() { - this.$emit('close') - } - } -}) \ No newline at end of file diff --git a/src/designer/viewWidgets/alarm/AlarmMeta.ts b/src/designer/viewWidgets/alarm/AlarmMeta.ts deleted file mode 100644 index 56cd72e..0000000 --- a/src/designer/viewWidgets/alarm/AlarmMeta.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineWidget } from '@/runtime/DefineWidget.ts' -import { renderIcon } from '@/utils/webutils.ts' -import AlarmView from './AlarmView.vue' - -export default defineWidget({ - name: 'alarm', - title: '告警', - icon: renderIcon('antd AlertOutlined'), - side: 'right', - order: 2, - component: AlarmView -}) \ No newline at end of file diff --git a/src/designer/viewWidgets/alarm/AlarmView.vue b/src/designer/viewWidgets/alarm/AlarmView.vue deleted file mode 100644 index 6125bd4..0000000 --- a/src/designer/viewWidgets/alarm/AlarmView.vue +++ /dev/null @@ -1,168 +0,0 @@ - - - \ No newline at end of file diff --git a/src/designer/viewWidgets/logger/LoggerMeta.ts b/src/designer/viewWidgets/logger/LoggerMeta.ts deleted file mode 100644 index eb75c82..0000000 --- a/src/designer/viewWidgets/logger/LoggerMeta.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineWidget } from '@/runtime/DefineWidget.ts' -import { renderIcon } from '@/utils/webutils.ts' -import LoggerView from './LoggerView.vue' - -export default defineWidget({ - name: 'logger', - title: '日志', - icon: renderIcon('element ToiletPaper'), - side: 'bottom', - order: 2, - component: LoggerView -}) \ No newline at end of file diff --git a/src/designer/viewWidgets/logger/LoggerView.vue b/src/designer/viewWidgets/logger/LoggerView.vue deleted file mode 100644 index 78a4a65..0000000 --- a/src/designer/viewWidgets/logger/LoggerView.vue +++ /dev/null @@ -1,69 +0,0 @@ - - \ No newline at end of file diff --git a/src/designer/viewWidgets/modeltree/ModeltreeMeta.ts b/src/designer/viewWidgets/modeltree/ModeltreeMeta.ts deleted file mode 100644 index 5a24afd..0000000 --- a/src/designer/viewWidgets/modeltree/ModeltreeMeta.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineWidget } from '@/runtime/DefineWidget.ts' -import { renderIcon } from '@/utils/webutils.ts' -import ModeltreeView from './ModeltreeView.vue' - -export default defineWidget({ - name: 'modeltree', - title: '模型', - icon: renderIcon('antd ClusterOutlined'), - side: 'left', - shortcut: 'key-F12', - order: 1, - component: ModeltreeView -}) \ No newline at end of file diff --git a/src/designer/viewWidgets/modeltree/ModeltreeView.vue b/src/designer/viewWidgets/modeltree/ModeltreeView.vue deleted file mode 100644 index f5104c7..0000000 --- a/src/designer/viewWidgets/modeltree/ModeltreeView.vue +++ /dev/null @@ -1,28 +0,0 @@ - - \ No newline at end of file diff --git a/src/designer/viewWidgets/modeltree/ModeltreeViewJs.js b/src/designer/viewWidgets/modeltree/ModeltreeViewJs.js deleted file mode 100644 index 9a0bc8c..0000000 --- a/src/designer/viewWidgets/modeltree/ModeltreeViewJs.js +++ /dev/null @@ -1,99 +0,0 @@ -import { defineComponent } from 'vue' -import { renderIcon } from '@/utils/webutils.js' -import IWidgets from '../IWidgets.js' - - -export default defineComponent({ - name: 'ModeltreeView', - mixins: [IWidgets], - data() { - return { - currentLevel: '', - searchKeyword: '', - treedata: data - } - }, - methods: { - allowDrop(event) { - return true - }, - allowDrag(event) { - return true - }, - handleDragStart() { - }, - handleDragEnter() { - }, - handleDragLeave() { - }, - handleDragOver() { - }, - handleDragEnd() { - }, - handleDrop() { - } - }, - computed: { - allLevels() { - return worldModel.state.allLevels - } - } -}) - -const data = [ - { - label: 'Level one 1', - children: [ - { - label: 'Level two 1-1', - children: [ - { - label: 'Level three 1-1-1' - } - ] - } - ] - }, - { - label: 'Level one 2', - children: [ - { - label: 'Level two 2-1', - children: [ - { - label: 'Level three 2-1-1' - } - ] - }, - { - label: 'Level two 2-2', - children: [ - { - label: 'Level three 2-2-1' - } - ] - } - ] - }, - { - label: 'Level one 3', - children: [ - { - label: 'Level two 3-1', - children: [ - { - label: 'Level three 3-1-1' - } - ] - }, - { - label: 'Level two 3-2', - children: [ - { - label: 'Level three 3-2-1' - } - ] - } - ] - } -] \ No newline at end of file diff --git a/src/designer/viewWidgets/monitor/MonitorMeta.ts b/src/designer/viewWidgets/monitor/MonitorMeta.ts deleted file mode 100644 index 07cddde..0000000 --- a/src/designer/viewWidgets/monitor/MonitorMeta.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineWidget } from '@/runtime/DefineWidget.ts' -import { renderIcon } from '@/utils/webutils.ts' -import MonitorView from './MonitorView.vue' - -export default defineWidget({ - name: 'monitor', - title: '监控', - icon: renderIcon('antd DashboardOutlined'), - side: 'left', - order: 2, - component: MonitorView -}) \ No newline at end of file diff --git a/src/designer/viewWidgets/monitor/MonitorView.vue b/src/designer/viewWidgets/monitor/MonitorView.vue deleted file mode 100644 index bf7e94e..0000000 --- a/src/designer/viewWidgets/monitor/MonitorView.vue +++ /dev/null @@ -1,243 +0,0 @@ - - - \ No newline at end of file diff --git a/src/designer/viewWidgets/property/PropertyMeta.ts b/src/designer/viewWidgets/property/PropertyMeta.ts deleted file mode 100644 index 073c016..0000000 --- a/src/designer/viewWidgets/property/PropertyMeta.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineWidget } from '@/runtime/DefineWidget.ts' -import { renderIcon } from '@/utils/webutils.ts' -import PropertyView from './PropertyView.vue' - -export default defineWidget({ - name: 'property', - title: '属性', - icon: renderIcon('element Memo'), - shortcut: 'key-F4', - side: 'right', - order: 1, - component: PropertyView -}) \ No newline at end of file diff --git a/src/designer/viewWidgets/property/PropertyView.vue b/src/designer/viewWidgets/property/PropertyView.vue deleted file mode 100644 index 5217643..0000000 --- a/src/designer/viewWidgets/property/PropertyView.vue +++ /dev/null @@ -1,143 +0,0 @@ - - - \ No newline at end of file diff --git a/src/designer/viewWidgets/script/ScriptMeta.ts b/src/designer/viewWidgets/script/ScriptMeta.ts deleted file mode 100644 index aaebd89..0000000 --- a/src/designer/viewWidgets/script/ScriptMeta.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineWidget } from '../../../runtime/DefineWidget.ts' -import { renderIcon } from '@/utils/webutils.ts' -import ScriptView from './ScriptView.vue' - -export default defineWidget({ - name: 'script', - title: '脚本', - icon: renderIcon('antd CodeOutlined'), - side: 'bottom', - order: 3, - component: ScriptView -}) \ No newline at end of file diff --git a/src/designer/viewWidgets/script/ScriptView.vue b/src/designer/viewWidgets/script/ScriptView.vue deleted file mode 100644 index 0c7d762..0000000 --- a/src/designer/viewWidgets/script/ScriptView.vue +++ /dev/null @@ -1,29 +0,0 @@ - - \ No newline at end of file diff --git a/src/designer/viewWidgets/task/TaskMeta.ts b/src/designer/viewWidgets/task/TaskMeta.ts deleted file mode 100644 index 602efdf..0000000 --- a/src/designer/viewWidgets/task/TaskMeta.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineWidget } from '@/runtime/DefineWidget.ts' -import { renderIcon } from '@/utils/webutils.ts' -import TaskView from './TaskView.vue' - -export default defineWidget({ - name: 'task', - title: '任务', - icon: renderIcon('element List'), - side: 'bottom', - order: 1, - component: TaskView -}) \ No newline at end of file diff --git a/src/designer/viewWidgets/task/TaskView.vue b/src/designer/viewWidgets/task/TaskView.vue deleted file mode 100644 index c3a85a0..0000000 --- a/src/designer/viewWidgets/task/TaskView.vue +++ /dev/null @@ -1,183 +0,0 @@ - - - \ No newline at end of file diff --git a/src/designer/viewWidgets/toolbox/ToolboxMeta.ts b/src/designer/viewWidgets/toolbox/ToolboxMeta.ts deleted file mode 100644 index 9ccd171..0000000 --- a/src/designer/viewWidgets/toolbox/ToolboxMeta.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineWidget } from '@/runtime/DefineWidget.ts' -import { renderIcon } from '@/utils/webutils.ts' -import ToolboxView from './ToolboxView.vue' - -export default defineWidget({ - name: 'toolbox', - title: '工具箱', - icon: renderIcon('antd CodepenOutlined'), - side: 'left', - order: 3, - component: ToolboxView -}) \ No newline at end of file diff --git a/src/designer/viewWidgets/toolbox/ToolboxView.vue b/src/designer/viewWidgets/toolbox/ToolboxView.vue deleted file mode 100644 index 10c372d..0000000 --- a/src/designer/viewWidgets/toolbox/ToolboxView.vue +++ /dev/null @@ -1,173 +0,0 @@ - - - \ No newline at end of file diff --git a/src/editor/Model2DEditor.vue b/src/editor/Model2DEditor.vue new file mode 100644 index 0000000..89b4287 --- /dev/null +++ b/src/editor/Model2DEditor.vue @@ -0,0 +1,213 @@ + + \ No newline at end of file diff --git a/src/editor/Model3DViewer.vue b/src/editor/Model3DViewer.vue new file mode 100644 index 0000000..c1499b5 --- /dev/null +++ b/src/editor/Model3DViewer.vue @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/src/editor/ModelMain.less b/src/editor/ModelMain.less new file mode 100644 index 0000000..8c92ffd --- /dev/null +++ b/src/editor/ModelMain.less @@ -0,0 +1,355 @@ +.app-wrap { + height: 100%; + overflow: hidden; + display: flex; + flex-direction: column; + + .app-header { + height: 50px; + background: #545c64; + flex-shrink: 0; + display: flex; + flex-direction: row; + overflow: hidden; + + .logo { + display: flex; + align-items: center; + margin: 0 20px; + } + + .app-header-menu-wrap { + flex: 1; + display: flex; + flex-direction: row; + + .app-header-menu { + height: 100%; + padding: 0 16px; + color: #fff; + display: flex; + align-items: center; + cursor: pointer; + + .el-icon { + margin-left: 8px; + font-size: 12px; + } + + &:hover { + background-color: #494d52; + color: #fff + } + } + } + + .user { + display: flex; + flex-direction: row; + align-items: center; + margin-right: 10px; + + & > span { + display: inline-flex; + padding: 5px; + background: #f4c521; + border-radius: 15px; + color: #fff; + } + } + } + + .app-section { + flex: 1; + display: flex; + flex-direction: row; + overflow: hidden; + + .btns-toolbar { + display: flex; + flex-direction: column; + + .btns { + .item { + height: 48px; + line-height: 48px; + text-align: center; + cursor: pointer; + font-size: 26px; + + &:hover { + background: #cccccc; + } + + &.selected { + background: #e8e8e8; + position: relative; + + &:before { + content: ''; + position: absolute; + width: 3px; + height: 100%; + background: #f4c521; + left: 0; + top: 0; + } + } + } + } + + .btns-top { + flex: 1; + } + + .btns-bottom { + + } + } + + .btns-toolbar-left { + flex-shrink: 0; + width: 50px; + border-right: 1px solid #dcdcdc + } + + .btns-toolbar-right { + flex-shrink: 0; + width: 50px; + border-left: 1px solid #dcdcdc; + + &.btns-toolbar { + .btns .item.selected:before { + right: 0; + left: auto; + } + } + } + + .section { + flex: 1; + overflow: hidden; + + .section-item-wrap { + height: 100%; + display: flex; + flex-direction: column; + + & > .title { + border-bottom: 1px solid #dcdcdc; + height: 35px; + line-height: 35px; + padding: 0 0 0 10px; + font-size: 14px; + position: relative; + display: flex; + align-items: center; + + & > .el-icon { + margin-right: 3px; + position: relative; + top: 1px; + } + + & > .el-input { + flex: 1; + margin: 0 30px 0 10px; + } + + .close { + position: absolute; + right: 0; + display: inline-flex; + padding: 10px; + cursor: pointer; + + &:hover { + color: var(--el-color-primary) + } + } + } + + .calc-left-panel { + flex: 1; + overflow: auto; + } + + .calc-right-panel { + flex: 1; + overflow: auto; + } + + .calc-bottom-panel { + flex: 1; + overflow: auto; + } + } + + .section-bottom { + .section-item-wrap { + & > .title > .el-input { + flex: none; + } + } + } + + .section-tabs.el-tabs--card { + height: 100%; + + & > .el-tabs__header { + box-sizing: border-box; + z-index: 0; + margin: 0; + + & > .el-tabs__nav-wrap { + margin-bottom: 0 + } + + .el-tabs__item.is-active { + position: relative; + z-index: 1; + + &:before { + content: ''; + width: 100%; + height: 1px; + background: #c61429; + position: absolute; + left: 0; + top: 0; + z-index: 999; + } + + &:after { + content: ''; + width: 100%; + height: 1px; + background: #fff; + position: absolute; + left: 0; + bottom: 0; + z-index: 999; + } + + &:hover { + &:after { + background: #c5c5c5; + } + } + } + + .el-tabs__item { + border-bottom: 0; + } + + .el-tabs__nav-prev { + height: 40px; + background: #c9c9c9; + + .el-icon { + color: #c61429 + } + } + + .el-tabs__nav-next { + height: 40px; + background: #c9c9c9; + + .el-icon { + color: #c61429 + } + } + } + + & > .el-tabs__content { + flex: 1; + + & > .el-tab-pane { + height: 100%; + } + + .section-canvas { + height: 100%; + display: flex; + flex-direction: column; + + .section-toolbar { + flex-shrink: 0; + height: 30px; + display: flex; + align-items: center; + + .el-button { + margin-left: 5px; + } + + .section-toolbar-line { + width: 1px; + height: 16px; + background: #dcdcdc; + margin: 0 5px; + } + + &.section-bottom-toolbar { + justify-content: space-between; + + .section-toolbar-left { + display: flex; + align-items: center; + } + + .section-toolbar-right { + display: flex; + flex-direction: row; + align-items: center; + + .infor { + background: #000; + margin: 0 5px; + color: #fff; + font-size: 12px; + min-width: 120px; + text-align: center; + padding: 3px 5px; + } + } + } + } + + .section-content { + flex: 1; + background: #e0e0e0; + display: flex; + overflow: hidden; + + & > .canvas-container { + flex: 1; + position: relative; + } + } + } + } + } + } + } +} + +.el-popper .el-divider--horizontal { + margin: 5px 0; + border-color: #656668 +} + +.model-3d-view-wrap.el-dialog.resize-dialog { + padding: 0 !important; + + .el-dialog__footer { + //padding-top:8px; + display: none; + } + + .el-dialog__header { + display: flex; + align-items: center; + padding: 8px 0 8px 8px; + } +} \ No newline at end of file diff --git a/src/editor/ModelMain.vue b/src/editor/ModelMain.vue new file mode 100644 index 0000000..eb87d2a --- /dev/null +++ b/src/editor/ModelMain.vue @@ -0,0 +1,307 @@ + + \ No newline at end of file diff --git a/src/editor/ModelMainInit.ts b/src/editor/ModelMainInit.ts new file mode 100644 index 0000000..e91c2c3 --- /dev/null +++ b/src/editor/ModelMainInit.ts @@ -0,0 +1,65 @@ +import _ from 'lodash' +import hotkeys from 'hotkeys-js' +import AlarmMeta from './widgets/alarm/AlarmMeta' +import LoggerMeta from './widgets/logger/LoggerMeta' +import ModeltreeMeta from './widgets/modeltree/ModeltreeMeta' +import MonitorMeta from './widgets/monitor/MonitorMeta' +import PropertyMeta from './widgets/property/PropertyMeta' +import ScriptMeta from './widgets/script/ScriptMeta' +import TaskMeta from './widgets/task/TaskMeta' +import ToolboxMeta from './widgets/toolbox/ToolboxMeta' + +import FileMenu from './menus/FileMenu' +import EditMenu from './menus/EditMenu' +import ToolsMenu from './menus/Tools' +import Model3DView from './menus/Model3DView' +import { forEachMenu } from '@/runtime/DefineMenu' +import { normalizeShortKey } from '@/utils/webutils' +import WorldModel from '@/core/manager/WorldModel' + +/** + * 初始化模型编辑器的基础控件 + */ +export function ModelMainInit() { + AlarmMeta.install() + LoggerMeta.install() + ModeltreeMeta.install() + MonitorMeta.install() + PropertyMeta.install() + ScriptMeta.install() + TaskMeta.install() + ToolboxMeta.install() + + FileMenu.install() + EditMenu.install() + ToolsMenu.install() + Model3DView.install() + + const worldModel = new WorldModel() + window['worldModel'] = worldModel +} + +export function ModelMainMounted() { + + forEachMenu((menu) => { + if (typeof menu.click === 'function') { + + const shortKey = normalizeShortKey(menu.tip) + if (shortKey) { + menu.tip = shortKey + hotkeys(shortKey, (event) => { + event.preventDefault() + menu.click() + }) + // console.log('hotkeys', menu.tip, menu.click) + } + } + }) + + return worldModel.init() +} + +export function ModelMainUnmounted() { + // 移除所有的热键绑定 unbind all + hotkeys.unbind() +} \ No newline at end of file diff --git a/src/editor/menus/EditMenu.ts b/src/editor/menus/EditMenu.ts new file mode 100644 index 0000000..5998c64 --- /dev/null +++ b/src/editor/menus/EditMenu.ts @@ -0,0 +1,111 @@ +import { renderIcon } from '@/utils/webutils.ts' +import { defineMenu } from '@/runtime/DefineMenu.ts' +import SvgCode from '@/components/icons/SvgCode' +import { escByKeyboard, quickCopyByMouse, deletePointByKeyboard } from '@/core/ModelUtils' + +export default defineMenu((menus) => { + menus.insertChildren('modelFile', + { + name: 'modelFile', + label: '编辑', + icon: renderIcon('ModelFile'), + order: 1 + }, + [ + { + name: 'find', label: '全局查找', icon: SvgCode.find, order: 1, tip: 'Ctrl+H', + click: () => { + system.msg('全局查找') + } + }, + { + name: 'resource', label: '资源定位', icon: SvgCode.find, order: 1.1, tip: 'Ctrl+Shift+R', divided: true, + click: () => { + system.msg('资源定位') + } + }, + { + name: 'undo', label: '撤销', icon: SvgCode.undo, order: 2, tip: 'Ctrl+Z', disabled: true, + click: () => { + system.msg('撤销') + } + }, + { + name: 'redo', label: '重做', icon: SvgCode.redo, order: 3, tip: 'Ctrl+Y', divided: true, + click() { + system.msg('重做') + } + }, + { + name: 'copy', label: '复制', icon: SvgCode.copy, order: 4, tip: 'Ctrl+C', + click() { + system.msg('复制') + } + }, + { + name: 'cut', label: '剪切', icon: SvgCode.cut, order: 5, tip: 'Ctrl+X', + click() { + system.msg('剪切') + } + }, + { + name: 'paste', label: '粘贴', icon: SvgCode.paste, order: 6, tip: 'Ctrl+V', + click() { + system.msg('粘贴') + } + }, + { + name: 'delete', label: '删除', icon: SvgCode.delete, order: 7, tip: 'key-delete', divided: true, + click() { + deletePointByKeyboard() + } + }, + { + name: 'edit_property', label: '快速转换', order: 8, + children: [ + { + name: 'edit_property_esc', label: '取消', order: 1, tip: 'key-esc', + click() { + escByKeyboard() + } + }, + { + name: 'edit_property_rotate', label: '转向90度', order: 1, tip: 'key-r', + click() { + system.msg('转向90度') + } + }, + { + name: 'edit_append', label: '快速添加', tip: 'key-q', + click() { + quickCopyByMouse() + } + }, + { + name: 'edit_up', label: '上移', tip: 'key-up', + click() { + system.msg('↑') + } + }, + { + name: 'edit_down', label: '下移', tip: 'key-down', + click() { + system.msg('↓') + } + }, + { + name: 'edit_left', label: '左移', tip: 'key-left', + click() { + system.msg('←') + } + }, + { + name: 'edit_right', label: '右移', tip: 'key-right', + click() { + system.msg('→') + } + } + ] + } + ]) +}) \ No newline at end of file diff --git a/src/editor/menus/FileMenu.ts b/src/editor/menus/FileMenu.ts new file mode 100644 index 0000000..5b1c564 --- /dev/null +++ b/src/editor/menus/FileMenu.ts @@ -0,0 +1,37 @@ +import { renderIcon } from '@/utils/webutils.ts' +import { defineMenu } from '@/runtime/DefineMenu.ts' +import SvgCode from '@/components/icons/SvgCode' + +export default defineMenu((menus) => { + menus.insertChildren('file', + { + name: 'file', label: '模型', icon: renderIcon('ModelFile'), order: 1, disabled: false + }, + [ + { + name: 'open', label: '打开', icon: SvgCode.open, order: 1, tip: 'Ctrl+O', + click: () => { + system.showLoading() + import('@/example/example1').then(res => { + worldModel.loadCatalog(res.default) + + }).finally(() => { + system.clearLoading() + }) + } + }, + { + name: 'save', label: '保存', icon: SvgCode.save, order: 2, tip: 'Ctrl+S', + click: () => { + system.msg('保存模型文件') + } + }, + { + name: 'saveAs', label: '另存为', icon: renderIcon('ModelFile'), order: 3, + click: () => { + system.msg('另存为模型文件') + } + } + ] + ) +}) \ No newline at end of file diff --git a/src/editor/menus/Model3DView.ts b/src/editor/menus/Model3DView.ts new file mode 100644 index 0000000..b274e13 --- /dev/null +++ b/src/editor/menus/Model3DView.ts @@ -0,0 +1,27 @@ +import { defineMenu } from '@/runtime/DefineMenu.ts' +import Model3DView from '@/components/Model3DView.vue' + +export default defineMenu((menus) => { + menus.insertChildren('tool', + { + name: 'tool', label: '小工具', order: 3, disabled: false + }, + [ + { + name: 'model3dview', label: '模型查看器', order: 1, + click: () => { + system.showDialog(Model3DView, { + title: '模型查看器', + width: 950, + height: 400, + showClose: true, + showMax: true, + showCancelButton: false, + showOkButton: false, + dialogClass: 'model-3d-view-wrap' + }) + } + } + ] + ) +}) \ No newline at end of file diff --git a/src/editor/menus/Tools.ts b/src/editor/menus/Tools.ts new file mode 100644 index 0000000..23fc9f5 --- /dev/null +++ b/src/editor/menus/Tools.ts @@ -0,0 +1,11 @@ +import { defineMenu } from '@/runtime/DefineMenu.ts' +import { renderIcon } from '@/utils/webutils.ts' + +export default defineMenu((menus) => { + menus.insertChildren('file', + { + name: 'tool', label: '小工具', order: 3, disabled: false + }, + [] + ) +}) \ No newline at end of file diff --git a/src/editor/propEditors/ColorItem.vue b/src/editor/propEditors/ColorItem.vue new file mode 100644 index 0000000..1423a26 --- /dev/null +++ b/src/editor/propEditors/ColorItem.vue @@ -0,0 +1,34 @@ + + \ No newline at end of file diff --git a/src/editor/propEditors/IMetaProp.ts b/src/editor/propEditors/IMetaProp.ts new file mode 100644 index 0000000..7d4fed2 --- /dev/null +++ b/src/editor/propEditors/IMetaProp.ts @@ -0,0 +1,34 @@ +import * as THREE from 'three' +import { type ItemTypeMetaItem } from '@/model/itemType/ItemTypeDefine' +import { defineComponent, type PropType } from 'vue' +import type Viewport from '@/core/engine/Viewport' +import EventBus from '@/runtime/EventBus' + +export default defineComponent({ + props: { + prop: Object as PropType, + viewport: Object as PropType + }, + mounted() { + EventBus.on('objectChanged', (data) => { + //@ts-ignore + if (typeof this.refreshValue === 'function') { + //@ts-ignore + this.refreshValue() + } + }) + + this.$nextTick(() => { + //@ts-ignore + if (typeof this.refreshValue === 'function') { + //@ts-ignore + this.refreshValue() + } + }) + }, + computed: { + object3D(): THREE.Object3D { + return this.viewport.state.selectedObject + } + } +}) \ No newline at end of file diff --git a/src/editor/propEditors/NumberInput.vue b/src/editor/propEditors/NumberInput.vue new file mode 100644 index 0000000..62a5dac --- /dev/null +++ b/src/editor/propEditors/NumberInput.vue @@ -0,0 +1,33 @@ + + \ No newline at end of file diff --git a/src/editor/propEditors/SwitchItem.vue b/src/editor/propEditors/SwitchItem.vue new file mode 100644 index 0000000..7fad9e1 --- /dev/null +++ b/src/editor/propEditors/SwitchItem.vue @@ -0,0 +1,33 @@ + + \ No newline at end of file diff --git a/src/editor/propEditors/TextInput.vue b/src/editor/propEditors/TextInput.vue new file mode 100644 index 0000000..b67a1cc --- /dev/null +++ b/src/editor/propEditors/TextInput.vue @@ -0,0 +1,33 @@ + + \ No newline at end of file diff --git a/src/editor/propEditors/Transform.vue b/src/editor/propEditors/Transform.vue new file mode 100644 index 0000000..e2f9404 --- /dev/null +++ b/src/editor/propEditors/Transform.vue @@ -0,0 +1,170 @@ + + + \ No newline at end of file diff --git a/src/editor/propEditors/UUIDItem.vue b/src/editor/propEditors/UUIDItem.vue new file mode 100644 index 0000000..841f55d --- /dev/null +++ b/src/editor/propEditors/UUIDItem.vue @@ -0,0 +1,33 @@ + + \ No newline at end of file diff --git a/src/editor/widgets/IWidgets.ts b/src/editor/widgets/IWidgets.ts new file mode 100644 index 0000000..fa8a44e --- /dev/null +++ b/src/editor/widgets/IWidgets.ts @@ -0,0 +1,40 @@ +import { defineComponent } from 'vue' +import { renderIcon } from '@/utils/webutils.js' +import Viewport, { type ViewportState } from '@/core/engine/Viewport.ts' + +export type IWidgetData = { + /** + * 是否激活 + */ + isActivated: boolean +} + +export default defineComponent({ + activated() { + this.isActivated = true + console.log('activated', this.$.type.name) + }, + deactivated() { + this.isActivated = false + }, + props: { + viewport: Viewport + }, + computed: { + state(): ViewportState { + return this.viewport?.state + } + }, + emits: ['close'], + data(): IWidgetData { + return { + isActivated: false + } + }, + methods: { + renderIcon, + closeMe() { + this.$emit('close') + } + } +}) \ No newline at end of file diff --git a/src/editor/widgets/alarm/AlarmMeta.ts b/src/editor/widgets/alarm/AlarmMeta.ts new file mode 100644 index 0000000..56cd72e --- /dev/null +++ b/src/editor/widgets/alarm/AlarmMeta.ts @@ -0,0 +1,12 @@ +import { defineWidget } from '@/runtime/DefineWidget.ts' +import { renderIcon } from '@/utils/webutils.ts' +import AlarmView from './AlarmView.vue' + +export default defineWidget({ + name: 'alarm', + title: '告警', + icon: renderIcon('antd AlertOutlined'), + side: 'right', + order: 2, + component: AlarmView +}) \ No newline at end of file diff --git a/src/editor/widgets/alarm/AlarmView.vue b/src/editor/widgets/alarm/AlarmView.vue new file mode 100644 index 0000000..6125bd4 --- /dev/null +++ b/src/editor/widgets/alarm/AlarmView.vue @@ -0,0 +1,168 @@ + + + \ No newline at end of file diff --git a/src/editor/widgets/logger/LoggerMeta.ts b/src/editor/widgets/logger/LoggerMeta.ts new file mode 100644 index 0000000..eb75c82 --- /dev/null +++ b/src/editor/widgets/logger/LoggerMeta.ts @@ -0,0 +1,12 @@ +import { defineWidget } from '@/runtime/DefineWidget.ts' +import { renderIcon } from '@/utils/webutils.ts' +import LoggerView from './LoggerView.vue' + +export default defineWidget({ + name: 'logger', + title: '日志', + icon: renderIcon('element ToiletPaper'), + side: 'bottom', + order: 2, + component: LoggerView +}) \ No newline at end of file diff --git a/src/editor/widgets/logger/LoggerView.vue b/src/editor/widgets/logger/LoggerView.vue new file mode 100644 index 0000000..78a4a65 --- /dev/null +++ b/src/editor/widgets/logger/LoggerView.vue @@ -0,0 +1,69 @@ + + \ No newline at end of file diff --git a/src/editor/widgets/modeltree/ModeltreeMeta.ts b/src/editor/widgets/modeltree/ModeltreeMeta.ts new file mode 100644 index 0000000..5a24afd --- /dev/null +++ b/src/editor/widgets/modeltree/ModeltreeMeta.ts @@ -0,0 +1,13 @@ +import { defineWidget } from '@/runtime/DefineWidget.ts' +import { renderIcon } from '@/utils/webutils.ts' +import ModeltreeView from './ModeltreeView.vue' + +export default defineWidget({ + name: 'modeltree', + title: '模型', + icon: renderIcon('antd ClusterOutlined'), + side: 'left', + shortcut: 'key-F12', + order: 1, + component: ModeltreeView +}) \ No newline at end of file diff --git a/src/editor/widgets/modeltree/ModeltreeView.vue b/src/editor/widgets/modeltree/ModeltreeView.vue new file mode 100644 index 0000000..a84fb5b --- /dev/null +++ b/src/editor/widgets/modeltree/ModeltreeView.vue @@ -0,0 +1,30 @@ + + \ No newline at end of file diff --git a/src/editor/widgets/modeltree/ModeltreeViewJs.js b/src/editor/widgets/modeltree/ModeltreeViewJs.js new file mode 100644 index 0000000..0734559 --- /dev/null +++ b/src/editor/widgets/modeltree/ModeltreeViewJs.js @@ -0,0 +1,116 @@ +import { defineComponent } from 'vue' +import { renderIcon } from '@/utils/webutils.js' +import IWidgets from '../IWidgets.js' + + +export default defineComponent({ + name: 'ModeltreeView', + mixins: [IWidgets], + data() { + return { + searchKeyword: '', + treedata: data + } + }, + methods: { + allowDrop(event) { + return true + }, + allowDrag(event) { + return true + }, + handleDragStart() { + }, + handleDragEnter() { + }, + handleDragLeave() { + }, + handleDragOver() { + }, + handleDragEnd() { + }, + handleDrop() { + } + }, + computed: { + 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 + })) + })) + } + } +}) + +const data = [ + { + label: 'Level one 1', + children: [ + { + label: 'Level two 1-1', + children: [ + { + label: 'Level three 1-1-1' + } + ] + } + ] + }, + { + label: 'Level one 2', + children: [ + { + label: 'Level two 2-1', + children: [ + { + label: 'Level three 2-1-1' + } + ] + }, + { + label: 'Level two 2-2', + children: [ + { + label: 'Level three 2-2-1' + } + ] + } + ] + }, + { + label: 'Level one 3', + children: [ + { + label: 'Level two 3-1', + children: [ + { + label: 'Level three 3-1-1' + } + ] + }, + { + label: 'Level two 3-2', + children: [ + { + label: 'Level three 3-2-1' + } + ] + } + ] + } +] \ No newline at end of file diff --git a/src/editor/widgets/monitor/MonitorMeta.ts b/src/editor/widgets/monitor/MonitorMeta.ts new file mode 100644 index 0000000..07cddde --- /dev/null +++ b/src/editor/widgets/monitor/MonitorMeta.ts @@ -0,0 +1,12 @@ +import { defineWidget } from '@/runtime/DefineWidget.ts' +import { renderIcon } from '@/utils/webutils.ts' +import MonitorView from './MonitorView.vue' + +export default defineWidget({ + name: 'monitor', + title: '监控', + icon: renderIcon('antd DashboardOutlined'), + side: 'left', + order: 2, + component: MonitorView +}) \ No newline at end of file diff --git a/src/editor/widgets/monitor/MonitorView.vue b/src/editor/widgets/monitor/MonitorView.vue new file mode 100644 index 0000000..bf7e94e --- /dev/null +++ b/src/editor/widgets/monitor/MonitorView.vue @@ -0,0 +1,243 @@ + + + \ No newline at end of file diff --git a/src/editor/widgets/property/PropertyMeta.ts b/src/editor/widgets/property/PropertyMeta.ts new file mode 100644 index 0000000..073c016 --- /dev/null +++ b/src/editor/widgets/property/PropertyMeta.ts @@ -0,0 +1,13 @@ +import { defineWidget } from '@/runtime/DefineWidget.ts' +import { renderIcon } from '@/utils/webutils.ts' +import PropertyView from './PropertyView.vue' + +export default defineWidget({ + name: 'property', + title: '属性', + icon: renderIcon('element Memo'), + shortcut: 'key-F4', + side: 'right', + order: 1, + component: PropertyView +}) \ No newline at end of file diff --git a/src/editor/widgets/property/PropertyView.vue b/src/editor/widgets/property/PropertyView.vue new file mode 100644 index 0000000..95aafd8 --- /dev/null +++ b/src/editor/widgets/property/PropertyView.vue @@ -0,0 +1,143 @@ + + + \ No newline at end of file diff --git a/src/editor/widgets/script/ScriptMeta.ts b/src/editor/widgets/script/ScriptMeta.ts new file mode 100644 index 0000000..aaebd89 --- /dev/null +++ b/src/editor/widgets/script/ScriptMeta.ts @@ -0,0 +1,12 @@ +import { defineWidget } from '../../../runtime/DefineWidget.ts' +import { renderIcon } from '@/utils/webutils.ts' +import ScriptView from './ScriptView.vue' + +export default defineWidget({ + name: 'script', + title: '脚本', + icon: renderIcon('antd CodeOutlined'), + side: 'bottom', + order: 3, + component: ScriptView +}) \ No newline at end of file diff --git a/src/editor/widgets/script/ScriptView.vue b/src/editor/widgets/script/ScriptView.vue new file mode 100644 index 0000000..739af9c --- /dev/null +++ b/src/editor/widgets/script/ScriptView.vue @@ -0,0 +1,29 @@ + + \ No newline at end of file diff --git a/src/editor/widgets/task/TaskMeta.ts b/src/editor/widgets/task/TaskMeta.ts new file mode 100644 index 0000000..602efdf --- /dev/null +++ b/src/editor/widgets/task/TaskMeta.ts @@ -0,0 +1,12 @@ +import { defineWidget } from '@/runtime/DefineWidget.ts' +import { renderIcon } from '@/utils/webutils.ts' +import TaskView from './TaskView.vue' + +export default defineWidget({ + name: 'task', + title: '任务', + icon: renderIcon('element List'), + side: 'bottom', + order: 1, + component: TaskView +}) \ No newline at end of file diff --git a/src/editor/widgets/task/TaskView.vue b/src/editor/widgets/task/TaskView.vue new file mode 100644 index 0000000..c3a85a0 --- /dev/null +++ b/src/editor/widgets/task/TaskView.vue @@ -0,0 +1,183 @@ + + + \ No newline at end of file diff --git a/src/editor/widgets/toolbox/ToolboxMeta.ts b/src/editor/widgets/toolbox/ToolboxMeta.ts new file mode 100644 index 0000000..9ccd171 --- /dev/null +++ b/src/editor/widgets/toolbox/ToolboxMeta.ts @@ -0,0 +1,12 @@ +import { defineWidget } from '@/runtime/DefineWidget.ts' +import { renderIcon } from '@/utils/webutils.ts' +import ToolboxView from './ToolboxView.vue' + +export default defineWidget({ + name: 'toolbox', + title: '工具箱', + icon: renderIcon('antd CodepenOutlined'), + side: 'left', + order: 3, + component: ToolboxView +}) \ No newline at end of file diff --git a/src/editor/widgets/toolbox/ToolboxView.vue b/src/editor/widgets/toolbox/ToolboxView.vue new file mode 100644 index 0000000..10c372d --- /dev/null +++ b/src/editor/widgets/toolbox/ToolboxView.vue @@ -0,0 +1,173 @@ + + + \ No newline at end of file diff --git a/src/example/example1.js b/src/example/example1.js new file mode 100644 index 0000000..f0e954f --- /dev/null +++ b/src/example/example1.js @@ -0,0 +1,131 @@ +export default { + project_uuid: 'example1', + Tool: { + Group: [], + GlobalVariables: [], + UserCommand: [], + ProcessFlow: [], + Dashboard: [], + DataTable: [], + Trigger: [ + { name: 'OnOpen', fn: '' }, + { name: 'OnReset', fn: '' }, + { name: 'OnStart', fn: '' }, + { name: 'OnStop', fn: '' } + ], + gridHelper: { + 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 + } + }, + items: [ + { + catalogCode: 'f1', t: 'floor', // 楼层 + items: [ + { + name: 'measure-group', t: 'measure', a: 'gp', // 类型, itemType.name == 'measure' 的组件处理. a:'gp' 代表分组, 渲染时他会是 Three.Group + items: [ + { + 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 中 + center: ['p2'], // 用于 a='ln' 的测量线段, 关联的点对象(uuid) + in: [], // 物流入方向关联的对象(uuid) + out: [] // 物流出方向关联的对象(uuid) + } + }, + { + id: 'p2', + t: 'measure', a: 'ln', l: '测量2', c: '#ff0000', + tf: [[-9.0, 0, 3], [0, 0, 0], [0.25, 0.1, 0.25]], + dt: { + center: ['p3', 'p4'] + } + }, + { + id: 'p3', + t: 'measure', a: 'ln', l: '测量3', c: '#ff0000', + tf: [[-5.0, 0, 3], [0, 0, 0], [0.25, 0.1, 0.25]], + dt: { + center: [] + } + }, + { + id: 'p4', + t: 'measure', a: 'ln', l: '测量3', c: '#ff0000', + tf: [[-9.0, 0, 8], [0, 0, 0], [0.25, 0.1, 0.25]], + dt: { + center: [] + } + } + ] + } + ] + } + ], + elevator: [], + wall: [], + pillar: [], + catalog: [ + { + label: '仓库楼层', + items: [ + { catalogCode: '-f1', label: '地下室 (-f1)' }, + { catalogCode: 'f1', label: '一楼 (f1)' }, + { catalogCode: 'f2', label: '二楼 (f2)' }, + { catalogCode: 'OUT', label: '外场 (OUT)' }, + { catalogCode: 'fe', label: '楼层电梯 (fe)' } + ] + }, + { + label: '密集库区域', + items: [ + { catalogCode: 'm1', label: 'M1 (m1)' }, + { catalogCode: 'm2', label: 'M2 (m2)' }, + { catalogCode: 'm3', label: 'M3 (m3)' }, + { catalogCode: 'm4', label: 'M4 (m4)' }, + { catalogCode: 'me', label: '提升机 (me)' } + ] + }, + { + label: '多穿库A', + items: [ + { catalogCode: 'd1', label: 'D1 (d1)' }, + { catalogCode: 'd2', label: 'D2 (d2)' }, + { catalogCode: 'd3', label: 'D3 (d3)' }, + { catalogCode: 'd4', label: 'D4 (d4)' }, + { catalogCode: 'de1', label: '提升机 (de1)' } + ] + }, + { + label: '多穿库B', + items: [ + { catalogCode: 'e1', label: 'E1 (e1)' }, + { catalogCode: 'e2', label: 'E2 (e2)' }, + { catalogCode: 'e3', label: 'E3 (e3)' }, + { catalogCode: 'e4', label: 'E4 (e4)' }, + { catalogCode: 'ee1', label: '提升机 (ee1)' } + ] + } + ] +} \ No newline at end of file diff --git a/src/model/ModelUtils.ts b/src/model/ModelUtils.ts deleted file mode 100644 index 31c01c4..0000000 --- a/src/model/ModelUtils.ts +++ /dev/null @@ -1,292 +0,0 @@ -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 { 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) -} - -export function escByKeyboard() { - // 按下 ESC 键,取消当前操作 - const viewport: Viewport = window['viewport'] - if (!viewport) { - system.msg('没有找到当前视图') - return - } - - viewport.state.cursorMode = 'normal' - system.msg('操作已取消') -} - -export function quickCopyByMouse() { - // 获取鼠标位置,查看鼠标是否在某个 viewport 的画布上,并取得该 viewport - const currentMouseInfo = window['CurrentMouseInfo'] - if (!currentMouseInfo?.viewport || !currentMouseInfo.x || !currentMouseInfo.z) { - system.msg('无法获取鼠标位置') - return - } - - const x = currentMouseInfo.x - const z = currentMouseInfo.z - const viewport: Viewport = currentMouseInfo.viewport - // const point: THREE.Vector2 = currentMouseInfo.mouse - // - // const ray = new THREE.Raycaster() - // ray.setFromCamera(point, viewport.camera) - // const intersections = ray.intersectObjects(viewport.dragControl._dragObjects, true) - // - // if (intersections.length === 0) { - // system.msg('没有找到可复制的对象') - // return - // } - // console.log('intersections:', intersections) - - // 如果不在线上,查找0.2米内的有效点 Object3D, 如果有,则以这个点为起点, 延伸同类型的点,并让他们相连 - const r = findObject3DByCondition(viewport.scene, object => { - // 判断 object 是否是有效的 Object3D, 并且是当前 viewport 的对象 - if (object instanceof THREE.Object3D && object.visible && - object.userData.type && viewport.toolbox[object.userData.type]) { - - const toolbox: Toolbox = viewport.toolbox[object.userData.type] - - // 检查是否在 0.2 米内 - const distance = object.position.distanceTo(new THREE.Vector3(x, 0, z)) - if (distance < 0.2) { - // 找到一个有效点,执行复制操作 - viewport.toolStartObject = object - viewport.state.cursorMode = object.userData.type - // toolbox.start(object) - system.msg('连线成功') - return true - } - } - return false - }) - - if (!r || r.length === 0) { - system.msg('鼠标所在位置,没有可复制的对象') - return - } -} - -// -// /** -// * 查找射线周围指定半径内的对象 -// */ -// export function findObjectsInRadius(viewport: Viewport, -// point: THREE.Vector2, -// radius: number, -// lines: { object: THREE.Object3D, distance: number }[], -// points: { object: THREE.Object3D, distance: number }[] -// ): void { -// const ray = new THREE.Raycaster() -// ray.setFromCamera(point, viewport.camera) -// -// viewport.dragControl._dragObjects.forEach(obj => { -// if (obj instanceof THREE.Points) { -// // 处理点云:遍历每个点 -// const distance = distanceToRay(ray, point) -// if (distance <= radius) { -// points.push({ object: obj, distance }) -// } -// -// } else if (obj instanceof THREE.Line) { -// // 处理线段:计算线段到射线的最近距离 -// const distance = getLineDistanceToRay(ray, obj) -// if (distance <= radius) { -// lines.push({ object: obj, distance }) -// } -// } -// }) -// } -// -// /** -// * 计算点到射线的最短距离 -// */ -// function distanceToRay(ray: THREE.Raycaster, point: THREE.Vector2) { -// const closestPoint = new THREE.Vector3() -// ray.closestPointToPoint(point, closestPoint) -// return point.distanceTo(closestPoint) -// } -// -// /** -// * 计算线段到射线的最短距离 -// */ -// function getLineDistanceToRay(ray: THREE.Raycaster, line: THREE.Line) { -// const lineStart = new THREE.Vector3() -// const lineEnd = new THREE.Vector3() -// line.geometry.attributes.position.getXYZ(0, lineStart) -// line.geometry.attributes.position.getXYZ(1, lineEnd) -// line.localToWorld(lineStart) -// line.localToWorld(lineEnd) -// -// const lineSegment = new THREE.Line3(lineStart, lineEnd) -// const closestOnRay = new THREE.Vector3() -// const closestOnLine = new THREE.Vector3() -// THREE.Line3.prototype.closestPointsRayLine ??= function(ray, line, closestOnRay, closestOnLine) { -// // 实现射线与线段最近点计算(需自定义或使用数学库) -// } -// -// lineSegment.closestPointsRayLine(ray, true, closestOnRay, closestOnLine) -// return closestOnRay.distanceTo(closestOnLine) -// } - -/** - * 考虑吸附的情况下计算鼠标事件位置 - */ -export function calcPositionUseSnap(e: MouseEvent, point: THREE.Vector3) { - // 按下 ctrl 键,不启用吸附,其他情况启用吸附 - const gridOption = worldModel.gridOption - if (!e.ctrlKey && !e.metaKey) { - if (gridOption.snapEnabled && gridOption.snapDistance > 0) { - // 启用吸附, 针对 point 的 x 和 z 坐标进行吸附, 吸附距离为 gridOption.snapDistance - const snapDistance = gridOption.snapDistance - const newPoint = new THREE.Vector3(point.x, point.y, point.z) - newPoint.x = Math.round(newPoint.x / snapDistance) * snapDistance - newPoint.z = Math.round(newPoint.z / snapDistance) * snapDistance - return newPoint - } - } - - return point -} - -export function getAllControlPoints(): THREE.Object3D[] { - const allPoints: THREE.Object3D[] = [] - - getAllItemTypes().forEach((itemType: ItemTypeDefineOption) => { - if (itemType.clazz && itemType.clazz.pointArray) { - // 将每个 ItemType 的点添加到结果数组中 - allPoints.push(...itemType.clazz.pointArray) - } - }) - - return allPoints -} - -/** - * 在给定的场景中查找具有指定 uuid 的 Object3D 对象 - */ -export function findObject3DById(scene: THREE.Object3D, uuid: string): THREE.Object3D | undefined { - const rets = findObject3DByCondition(scene, object => object.uuid === uuid) - if (rets.length > 0) { - return rets[0] - } - return undefined -} - -/** - * 在给定场景中查找满足特定条件的 Object3D 对象集合 - */ -export function findObject3DByCondition(scene: THREE.Object3D, condition: (object: THREE.Object3D) => boolean): THREE.Object3D[] { - const foundObjects: THREE.Object3D[] = [] - - // 定义一个内部递归函数来遍历每个节点及其子节点 - function traverse(obj: THREE.Object3D) { - if (condition(obj)) { - foundObjects.push(obj) - } - - // 遍历当前对象的所有子对象 - for (let i = 0; i < obj.children.length; i++) { - traverse(obj.children[i]) - } - } - - // 开始从场景根节点进行遍历 - traverse(scene) - - return foundObjects -} - -export function loadSceneFromJson(viewport: Viewport, scene: THREE.Scene, items: ItemJson[]) { - console.time('loadSceneFromJson') - - const object3ds: THREE.Object3D[] = [] - - // beforeLoad 通知所有加载的对象, 模型加载开始 - getAllItemTypes().forEach((itemType: ItemTypeDefineOption) => { - const ret = itemType.clazz.beforeLoad() - Array.isArray(ret) && object3ds.push(...ret) - }) - - const loads = loadObject3DFromJson(items) - Array.isArray(loads) && object3ds.push(...loads) - - // afterLoadComplete 通知所有加载的对象, 模型加载完成 - getAllItemTypes().forEach((itemType: ItemTypeDefineOption) => { - const ret = itemType.clazz.afterLoadComplete(object3ds) - Array.isArray(ret) && object3ds.push(...ret) - }) - - scene.add(...object3ds) - - // afterAddScene 通知所有加载的对象, 模型加载完成 - getAllItemTypes().forEach(itemType => { - itemType.clazz.afterAddScene(viewport, scene, object3ds) - }) - - console.log('loadSceneFromJson:', items.length, 'items,', object3ds.length, 'objects') - console.timeEnd('loadSceneFromJson') -} - -function loadObject3DFromJson(items: ItemJson[]): THREE.Object3D[] { - const result: THREE.Object3D[] = [] - - for (const item of items) { - if (!item || !item.t) { - console.error('unkown item:', item) - continue - } - - const object3D: THREE.Object3D | undefined = getItemTypeByName(item.t)?.clazz.loadFromJson(item) - if (object3D === undefined) { - continue - } - - if (_.isArray(item.items)) { - // 如果有子元素,递归处理 - const children = loadObject3DFromJson(item.items) - children.forEach(child => object3D.add(child)) - } - - result.push(object3D) - } - - return result -} \ No newline at end of file diff --git a/src/model/WorldModel.ts b/src/model/WorldModel.ts deleted file mode 100644 index 5ae2ab3..0000000 --- a/src/model/WorldModel.ts +++ /dev/null @@ -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() - 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 = [] - } -} \ No newline at end of file diff --git a/src/model/WorldModelType.ts b/src/model/WorldModelType.ts deleted file mode 100644 index ac14f25..0000000 --- a/src/model/WorldModelType.ts +++ /dev/null @@ -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[] -} \ No newline at end of file diff --git a/src/model/example1.js b/src/model/example1.js deleted file mode 100644 index 16b22a2..0000000 --- a/src/model/example1.js +++ /dev/null @@ -1,130 +0,0 @@ -export default { - Tool: { - Group: [], - GlobalVariables: [], - UserCommand: [], - ProcessFlow: [], - Dashboard: [], - DataTable: [], - Trigger: [ - { name: 'OnOpen', fn: '' }, - { name: 'OnReset', fn: '' }, - { name: 'OnStart', fn: '' }, - { name: 'OnStop', fn: '' } - ], - gridHelper: { - 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 - } - }, - items: [ - { - name: 'f1', t: 'floor', // 楼层 - items: [ - { - name: 'measure-group', t: 'measure', a: 'gp', // 类型, itemType.name == 'measure' 的组件处理. a:'gp' 代表分组, 渲染时他会是 Three.Group - items: [ - { - 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 中 - center: ['p2'], // 用于 a='ln' 的测量线段, 关联的点对象(uuid) - in: [], // 物流入方向关联的对象(uuid) - out: [] // 物流出方向关联的对象(uuid) - } - }, - { - id: 'p2', - t: 'measure', a: 'ln', l: '测量2', c: '#ff0000', - tf: [[-9.0, 0, 3], [0, 0, 0], [0.25, 0.1, 0.25]], - dt: { - center: ['p3', 'p4'] - } - }, - { - id: 'p3', - t: 'measure', a: 'ln', l: '测量3', c: '#ff0000', - tf: [[-5.0, 0, 3], [0, 0, 0], [0.25, 0.1, 0.25]], - dt: { - center: [] - } - }, - { - id: 'p4', - t: 'measure', a: 'ln', l: '测量3', c: '#ff0000', - tf: [[-9.0, 0, 8], [0, 0, 0], [0.25, 0.1, 0.25]], - dt: { - center: [] - } - } - ] - } - ] - } - ], - elevator: [], - wall: [], - pillar: [], - 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)' } - ] - }, - { - value: 'D', label: '多穿库A', - children: [ - { value: 'd1', label: 'D1 (d1)' }, - { value: 'd2', label: 'D2 (d2)' }, - { value: 'd3', label: 'D3 (d3)' }, - { value: 'd4', label: 'D4 (d4)' }, - { value: 'de1', label: '提升机 (de1)' } - ] - }, - { - value: 'E', label: '多穿库B', - children: [ - { value: 'e1', label: 'E1 (e1)' }, - { value: 'e2', label: 'E2 (e2)' }, - { value: 'e3', label: 'E3 (e3)' }, - { value: 'e4', label: 'E4 (e4)' }, - { value: 'ee1', label: '提升机 (ee1)' } - ] - } - ] -} \ No newline at end of file diff --git a/src/model/itemType/ItemTypeLine.ts b/src/model/itemType/ItemTypeLine.ts index 10e6668..5ece0d4 100644 --- a/src/model/itemType/ItemTypeLine.ts +++ b/src/model/itemType/ItemTypeLine.ts @@ -99,6 +99,9 @@ export default abstract class ItemTypeLine extends ItemType { } } + /** + * 读取地图数据, 创建点单元 + */ createPoint(position: THREE.Vector3, item: ItemJson): THREE.Object3D { const point = this.createPointBasic(position) if (item.name) { diff --git a/src/model/itemType/ToolboxLine.ts b/src/model/itemType/ToolboxLine.ts index 7a07543..a178877 100644 --- a/src/model/itemType/ToolboxLine.ts +++ b/src/model/itemType/ToolboxLine.ts @@ -55,7 +55,7 @@ export default class ToolboxLine extends Toolbox { if (this.viewport.state.selectedObject === point) { // 如果当前选中的对象是要删除的点,则清除选中状态 this.viewport.state.selectedObject = undefined - EventBus.$emit('objectChanged', { + EventBus.dispatch('objectChanged', { viewport: this, object: null }) diff --git a/src/modules/measure/MeasureEntity.ts b/src/modules/measure/MeasureEntity.ts new file mode 100644 index 0000000..7f27506 --- /dev/null +++ b/src/modules/measure/MeasureEntity.ts @@ -0,0 +1,5 @@ +import BaseEntity from '@/core/base/BaseItemEntity.ts' + +export default class MeasureEntity extends BaseEntity { + +} \ No newline at end of file diff --git a/src/modules/measure/MeasureInteraction.ts b/src/modules/measure/MeasureInteraction.ts new file mode 100644 index 0000000..bd3f823 --- /dev/null +++ b/src/modules/measure/MeasureInteraction.ts @@ -0,0 +1,18 @@ +import * as THREE from 'three' +import type Viewport from '@/core/engine/Viewport.ts' +import BaseInteraction from '@/core/base/BaseInteraction.ts' + +export default class MeasureInteraction extends BaseInteraction { + dragPointComplete(viewport: Viewport): void { + } + + dragPointStart(viewport: Viewport, point: THREE.Object3D): void { + } + + start(viewport: Viewport, startPoint?: THREE.Object3D): void { + } + + stop(): void { + } + +} \ No newline at end of file diff --git a/src/modules/measure/MeasureMeta.ts b/src/modules/measure/MeasureMeta.ts new file mode 100644 index 0000000..b17d5bd --- /dev/null +++ b/src/modules/measure/MeasureMeta.ts @@ -0,0 +1,23 @@ +import type { IMeta } from '@/core/base/IMeta.ts' + +const MeasureMeta: IMeta = { + // "点"属性面板 + point: { + // 基础面板 + basic: [ + { field: 'uuid', editor: 'UUID', label: 'uuid', readonly: true }, + { field: 'name', editor: 'TextInput', label: '名称' }, + { field: 'dt.label', editor: 'TextInput', label: '标签' }, + { editor: 'TransformEditor' }, + { field: 'dt.color', editor: 'Color', label: '颜色' }, + { editor: '-' }, + { field: 'tf', editor: 'InOutCenterEditor' }, + { field: 'dt.selectable', editor: 'Switch', label: '可选中' }, + { field: 'dt.protected', editor: 'Switch', label: '受保护' }, + { field: 'visible', editor: 'Switch', label: '可见' } + ] + }, + // "线"属性面板 + line: {} +} +export default MeasureMeta \ No newline at end of file diff --git a/src/modules/measure/MeasureRenderer.ts b/src/modules/measure/MeasureRenderer.ts new file mode 100644 index 0000000..7bcf5d5 --- /dev/null +++ b/src/modules/measure/MeasureRenderer.ts @@ -0,0 +1,40 @@ +import type Viewport from '@/core/engine/Viewport.ts' +import BaseRenderer from '@/core/base/BaseRenderer.ts' + +/** + * 辅助测量工具渲染器 + */ +export default class MeasureRenderer extends BaseRenderer { + + // 开始更新, 可能暂停动画循环对本渲染器的动画等 + beginUpdate(viewport: Viewport) { + } + + // 创建一个点 + createPoint(item: ItemJson, option?: RendererCudOption) { + } + + // 删除一个点 + deletePoint(id, option?: RendererCudOption) { + } + + // 更新一个点 + updatePoint(item: ItemJson, option?: RendererCudOption) { + } + + // 创建一根线 + createLine(start: ItemJson, end: ItemJson, type: LinkType, option?: RendererCudOption) { + } + + // 更新一根线 + updateLine(start: ItemJson, end: ItemJson, type: LinkType, option?: RendererCudOption) { + } + + // 删除一根线 + deleteLine(start: ItemJson, end: ItemJson, type: LinkType, option?: RendererCudOption) { + } + + // 结束更新 + endUpdate(viewport: Viewport) { + } +} \ No newline at end of file diff --git a/src/modules/measure/index.ts b/src/modules/measure/index.ts new file mode 100644 index 0000000..1bf7ec9 --- /dev/null +++ b/src/modules/measure/index.ts @@ -0,0 +1,13 @@ +import { defineModule } from '@/core/manager/ModuleManager.ts' +import MeasureRenderer from './MeasureRenderer.ts' +import MeasureEntity from './MeasureEntity.ts' +import MeasureMeta from './MeasureMeta.ts' +import MeasureInteraction from './MeasureInteraction.ts' + +defineModule({ + name: 'measure', + renderer: new MeasureRenderer(), + interaction: new MeasureInteraction(), + meta: MeasureMeta, + entity: MeasureEntity +}) \ No newline at end of file diff --git a/src/router/index.ts b/src/router/index.ts index d445297..f80b5cc 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,6 +1,6 @@ import { createRouter, createWebHashHistory } from 'vue-router' // import HomeView from '../views/HomeView.vue' -import ModelMain from '../views/ModelMain.vue' +import ModelMain from '../editor/ModelMain.vue' const router = createRouter({ history: createWebHashHistory(import.meta.env.BASE_URL), diff --git a/src/runtime/EventBus.js b/src/runtime/EventBus.js deleted file mode 100644 index 585a5f4..0000000 --- a/src/runtime/EventBus.js +++ /dev/null @@ -1,9 +0,0 @@ -import mitt from 'mitt' - -const instance = mitt() - -export default { - $emit: instance.emit, - $on: instance.on, - $off: instance.off -} diff --git a/src/runtime/EventBus.ts b/src/runtime/EventBus.ts new file mode 100644 index 0000000..eac74dc --- /dev/null +++ b/src/runtime/EventBus.ts @@ -0,0 +1,17 @@ +import mitt from 'mitt' + +const instance = mitt() + +export type DispatchNames = 'objectChanged' | 'catalogChanged' + +export default { + dispatch(name: DispatchNames, data?: any) { + instance.emit(name, data) + }, + on(name: DispatchNames, callback: (data?: any) => void) { + instance.on(name, callback) + }, + off(name: DispatchNames, callback: (data?: any) => void) { + instance.off(name, callback) + } +} \ No newline at end of file diff --git a/src/runtime/System.ts b/src/runtime/System.ts index 29289c9..0e1e3db 100644 --- a/src/runtime/System.ts +++ b/src/runtime/System.ts @@ -6,7 +6,7 @@ import hotkeys from 'hotkeys-js' import { defineComponent, h, markRaw, nextTick, reactive, toRaw, unref, type App, createApp, type Component } from 'vue' import { ElMessage, ElMessageBox, ElNotification } from 'element-plus' import { QuestionFilled } from '@element-plus/icons-vue' -import { decompressUUID, renderIcon, createShortUUID } from '@/utils/webutils.ts' +import { decompressUUID, renderIcon, createShortUUID, setQueryParam, getQueryParams } from '@/utils/webutils.ts' import type { showDialogOption } from '@/SystemOption' import ShowDialogWrap from '@/components/ShowDialogWrap.vue' import LoadingDialog from '@/components/LoadingDialog.vue' @@ -40,6 +40,8 @@ export default class System { createUUID = createShortUUID decompressUUID = decompressUUID + setQueryParam = setQueryParam + getQueryParams = getQueryParams constructor(app: App) { this.app = app diff --git a/src/types/Types.d.ts b/src/types/Types.d.ts index 3bcd896..086367f 100644 --- a/src/types/Types.d.ts +++ b/src/types/Types.d.ts @@ -1,4 +1,4 @@ -export type CursorMode = +type CursorMode = 'normal' | 'ALink' | 'SLink' @@ -11,16 +11,7 @@ export type CursorMode = | 'MeasureAngle' | 'selectByRec' -type PointType = - 'measure-marker' - | 'pointMarker' - | 'pointMarker2' - | 'pointMarker3' - | 'pointMarker4' - | 'pointMarker5' - - -export export interface VData { +interface VData { /** * 场景数据 */ @@ -30,38 +21,65 @@ export export interface VData { * 是否发生了变化,通知外部是否需要保存数据 */ isChanged: boolean + + /** + * 对应存储id + */ + id: string + + /** + * 地图目录 + */ + catalog: Catalog +} + +interface CatalogItem { + catalogCode: string; // 楼层代码 + label: string; // 楼层显示名称 } -export interface VDataItem { +interface CatalogGroup { + label: string; // 楼层组名称 + items: CatalogItem[]; // 楼层组中的楼层列表 +} + +/** + * 世界模型目录数据 + */ +type Catalog = CatalogGroup[]; + +interface VDataItem { /** * { * id: 'p1', // 物体ID, 唯一标识, 需保证唯一, three.js 中的 uuid * t: 'measure', // 物体类型, measure表示测量, 需交给 itemType.name == 'measure' 的组件处理 - * l: '测量1', // 标签名称, 显示用 - * c: '#ff0000', // 颜色, 显示用. 十六进制颜色值, three.js 中的材质颜色 + * name: '', // 物体ID, 显示在 Tree 节点用 * tf: [ // 变换矩阵, 3x3矩阵, 采用Y轴向上为正, X轴向右, Z轴向前的右手坐标系 * [-9.0, 0, -1.0], // 平移向量 position * [0, 0, 0], // 旋转向量 rotation, 表示绕Y轴旋转的角度, 单位为度。对应 three.js 应进行"角度"转"弧度"的换算 * [0.25, 0.1, 0.25] // 缩放向量 scale * ], - * dt: { // 用户数据, 可自定义, 一般用在 three.js 的 userData 中 - * center: ['p2'], // 用于 a='ln' 的测量线段, 关联的点对象(uuid) - * in: [], // 物流入方向关联的对象(uuid) - * out: [] // 物流出方向关联的对象(uuid) + * dt: { // 用户数据, 可自定义, 一般用在 three.js 的 userData 中 + * label: '测量1', // 标签名称, 显示在 Tree/Map 用 + * color: '#ff0000', // 颜色, 显示用. 十六进制颜色值, three.js 中的材质颜色 + * center: ['p2'], // 用于 a='ln' 的测量线段, 关联的点对象(uuid) + * in: [], // 物流入方向关联的对象(uuid) + * out: [] // 物流出方向关联的对象(uuid) * ... // 其他自定义数据 * } * } */ id: string t: string - l: string - c: string + name: string tf: [ [number, number, number], [number, number, number], [number, number, number], ] dt: { + label: string + color: string center?: string[] // 用于 a='ln' 的测量线段, 关联的点对象(uuid) in?: string[] // 物流入方向关联的对象(uuid) out?: string[] // 物流出方向关联的对象(uuid) diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 092c219..ff22c30 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -1,7 +1,7 @@ import _ from 'lodash' import $ from 'jquery' import type System from '@/runtime/System' -import type WorldModel from '@/model/WorldModel' +import type WorldModel from '@/core/manager/WorldModel' declare global { const $: $ diff --git a/src/types/model.d.ts b/src/types/model.d.ts new file mode 100644 index 0000000..777087f --- /dev/null +++ b/src/types/model.d.ts @@ -0,0 +1,181 @@ +type LinkType = 'in' | 'out' | 'center' + +interface InitThreeOption { + stateManagerId: string +} + +interface InteractionCudOption { + +} + +/** + * 渲染器操作选项 + */ +interface RendererCudOption { + // Add any additional options needed for create, update, delete operations +} + +/** + * 实体操作选项 + */ +interface EntityCudOption { + // Additional options for create, update, delete operations +} + +interface IGridHelper { + /** + * 启用坐标轴 + */ + axesEnabled: boolean; + /** + * 坐标轴大小, 单位米 + */ + axesSize: number; + /** + * 坐标轴分割数 + */ + axesDivisions: number; + /** + * 坐标轴颜色, 十六进制颜色值 + */ + axesColor: number; + /** + * 坐标轴透明度 + */ + axesOpacity: number; + + /** + * 启用网格 + */ + gridEnabled: boolean; + /** + * 网格大小, 单位米 + */ + gridSize: number; + /** + * 网格分割数 + */ + gridDivisions: number; + /** + * 网格颜色, 十六进制颜色值 + */ + gridColor: number; + /** + * 网格透明度 + */ + gridOpacity: number; + + /** + * 启用吸附 + */ + snapEnabled: boolean; + /** + * 吸附距离, 单位米 + */ + snapDistance: number; +} + + +/** + * 物体单元(点) + * 举例: + * { + * id: 'p1', // 物体唯一ID, 也用于 three.js 中的 uuid + * t: 'measure', // 物体类型, measure表示测量, 需交给 itemType.name == 'measure' 的组件处理 + * tf: [ // 变换矩阵, 3x3矩阵, 采用Y轴向上为正, X轴向右, Z轴向前的右手坐标系 + * [-9.0, 0, -1.0], // 平移向量 position + * [0, 0, 0], // 旋转向量 rotation, 表示绕Y轴旋转的角度, 单位为度。对应 three.js 应进行"角度"转"弧度"的换算 + * [0.25, 0.1, 0.25] // 缩放向量 scale + * ], + * dt: { // 用户数据, 可自定义, 一般用在 three.js 的 userData 中 + * label: '测量1', // 标签名称, 显示用 + * color: '#ff0000', // 颜色, 显示用. 十六进制颜色值, three.js 中的材质颜色 + * center: ['p2'], // S连线(又称逻辑连线), 与其他点之间的无方向性关联, 关系的起点需要在他的 dt.center[] 数组中添加目标点的id, 关系的终点需要在他的 dt.center[] 数组中添加起点的 id + * in: [], // A连线(又称物体流动线)的输入, 关系的终点需要在 dt.in[] 数组中添加起点的 id + * out: [] // A连线(又称物体流动线)的输出, 关系的起点需要在 dt.out[] 数组中添加目标点的 id + * ...其他属性 + * } + * } + */ +interface ItemJson { + /** + * 对应 three.js 中的 uuid, 物体ID, 唯一标识, 需保证唯一, 有方法可以进行快速的 O(1) 查找 + */ + id?: string + + /** + * 物体名称, 显示用, 最后初始化到 three.js 的 name 中, 可以不设置, 可以不唯一, 但他的查找速度是 O(N) + */ + name?: string + + /** + * "点"的物体单元类型, 最终对应到 measure / conveyor / task 等不同的单元处理逻辑中 + */ + t: string + + /** + * 可见行, 对应 THREE.Object3D 的 visible + */ + v: boolean + + /** + * 变换矩阵, 3x3矩阵, 采用Y轴向上为正, X轴向右, Z轴向前的右手坐标系 + */ + tf: [ + /** + * 平移向量 position, 三维坐标 + */ + [number, number, number], + /** + * 旋转向量 rotation, 表示绕Y轴旋转的角度, 单位为度。对应 three.js 应进行"角度"转"弧度"的换算 + */ + [number, number, number], + /** + * 缩放向量 scale, 三维缩放比例 + */ + [number, number, number], + ] + + /** + * 用户数据, 可自定义, 一般用在 three.js 的 userData 中 + */ + dt: { + /** + * 标签名称, 显示用, 最后初始化到 three.js 的 userData.label 中, 最终应该如何渲染, 每个单元类型有自己的逻辑, 取决于物流单元类型t的 renderer 逻辑 + */ + label?: string + + /** + * 颜色, 最后初始化到 three.js 的 userData.color 中, 最终颜色应该如何渲染, 每个单元类型有自己的逻辑, 取决于物流单元类型t的 renderer 逻辑 + */ + color?: string + + /** + * S连线(又称逻辑连线), 与其他点之间的无方向性关联, 关系的起点需要在他的 dt.center[] 数组中添加目标点的id, 关系的终点需要在他的 dt.center[] 数组中添加起点的 id + */ + center?: string[] + /** + * A连线(又称物体流动线)的输入, 关系的终点需要在 dt.in[] 数组中添加起点的 id + */ + in?: string[] + /** + * A连线(又称物体流动线)的输出, 关系的起点需要在 dt.out[] 数组中添加目标点的 id + */ + out?: string[] + + /** + * 是否可以被选中, 默认 true + */ + selectable?: boolean + + /** + * 是否受保护, 不可在图形编辑器中拖拽, 默认 false + */ + protected?: boolean + + /** + * 其他自定义数据, 可以存储任何数据 + */ + [key: string]: any + }, +} \ No newline at end of file diff --git a/src/utils/webutils.ts b/src/utils/webutils.ts index 771b276..df909e7 100644 --- a/src/utils/webutils.ts +++ b/src/utils/webutils.ts @@ -6,6 +6,39 @@ import * as FaIcon from '@vicons/fa' import * as ElementPlusIconsVue from '@element-plus/icons-vue' import * as THREE from 'three' +export function getQueryParams() { + // const search = window.location.search || window.location.hash.split('?')[1] || '' + // const params = new URLSearchParams(search) + // return params.get(name) || null + const hash = window.location.hash || '#' + const match = hash.split('?')[1] || '' + return new URLSearchParams(match) +} + +export function setQueryParam(key: string, value: string) { + // const url = new URL(window.location.href) + // if (value) { + // // 如果 value 为空,则删除该参数 + // url.searchParams.set(key, value) + // } else { + // url.searchParams.delete(key) + // } + // const newUrl = url.toString() + // window.history.pushState({}, '', newUrl) + let [base, queryStr] = window.location.hash.split('?') + const params = new URLSearchParams(queryStr || '') + + if (!value) { + params.delete(key) // 删除参数 + } else { + params.set(key, value) // 设置或更新参数 + } + + const newQuery = params.toString() + const newHash = base + (newQuery ? '?' + newQuery : '') + window.location.hash = newHash +} + /** * 从 0x409EFF 数字变为字符串 '#409EFF' */ diff --git a/src/views/ModelMain.less b/src/views/ModelMain.less deleted file mode 100644 index 8c92ffd..0000000 --- a/src/views/ModelMain.less +++ /dev/null @@ -1,355 +0,0 @@ -.app-wrap { - height: 100%; - overflow: hidden; - display: flex; - flex-direction: column; - - .app-header { - height: 50px; - background: #545c64; - flex-shrink: 0; - display: flex; - flex-direction: row; - overflow: hidden; - - .logo { - display: flex; - align-items: center; - margin: 0 20px; - } - - .app-header-menu-wrap { - flex: 1; - display: flex; - flex-direction: row; - - .app-header-menu { - height: 100%; - padding: 0 16px; - color: #fff; - display: flex; - align-items: center; - cursor: pointer; - - .el-icon { - margin-left: 8px; - font-size: 12px; - } - - &:hover { - background-color: #494d52; - color: #fff - } - } - } - - .user { - display: flex; - flex-direction: row; - align-items: center; - margin-right: 10px; - - & > span { - display: inline-flex; - padding: 5px; - background: #f4c521; - border-radius: 15px; - color: #fff; - } - } - } - - .app-section { - flex: 1; - display: flex; - flex-direction: row; - overflow: hidden; - - .btns-toolbar { - display: flex; - flex-direction: column; - - .btns { - .item { - height: 48px; - line-height: 48px; - text-align: center; - cursor: pointer; - font-size: 26px; - - &:hover { - background: #cccccc; - } - - &.selected { - background: #e8e8e8; - position: relative; - - &:before { - content: ''; - position: absolute; - width: 3px; - height: 100%; - background: #f4c521; - left: 0; - top: 0; - } - } - } - } - - .btns-top { - flex: 1; - } - - .btns-bottom { - - } - } - - .btns-toolbar-left { - flex-shrink: 0; - width: 50px; - border-right: 1px solid #dcdcdc - } - - .btns-toolbar-right { - flex-shrink: 0; - width: 50px; - border-left: 1px solid #dcdcdc; - - &.btns-toolbar { - .btns .item.selected:before { - right: 0; - left: auto; - } - } - } - - .section { - flex: 1; - overflow: hidden; - - .section-item-wrap { - height: 100%; - display: flex; - flex-direction: column; - - & > .title { - border-bottom: 1px solid #dcdcdc; - height: 35px; - line-height: 35px; - padding: 0 0 0 10px; - font-size: 14px; - position: relative; - display: flex; - align-items: center; - - & > .el-icon { - margin-right: 3px; - position: relative; - top: 1px; - } - - & > .el-input { - flex: 1; - margin: 0 30px 0 10px; - } - - .close { - position: absolute; - right: 0; - display: inline-flex; - padding: 10px; - cursor: pointer; - - &:hover { - color: var(--el-color-primary) - } - } - } - - .calc-left-panel { - flex: 1; - overflow: auto; - } - - .calc-right-panel { - flex: 1; - overflow: auto; - } - - .calc-bottom-panel { - flex: 1; - overflow: auto; - } - } - - .section-bottom { - .section-item-wrap { - & > .title > .el-input { - flex: none; - } - } - } - - .section-tabs.el-tabs--card { - height: 100%; - - & > .el-tabs__header { - box-sizing: border-box; - z-index: 0; - margin: 0; - - & > .el-tabs__nav-wrap { - margin-bottom: 0 - } - - .el-tabs__item.is-active { - position: relative; - z-index: 1; - - &:before { - content: ''; - width: 100%; - height: 1px; - background: #c61429; - position: absolute; - left: 0; - top: 0; - z-index: 999; - } - - &:after { - content: ''; - width: 100%; - height: 1px; - background: #fff; - position: absolute; - left: 0; - bottom: 0; - z-index: 999; - } - - &:hover { - &:after { - background: #c5c5c5; - } - } - } - - .el-tabs__item { - border-bottom: 0; - } - - .el-tabs__nav-prev { - height: 40px; - background: #c9c9c9; - - .el-icon { - color: #c61429 - } - } - - .el-tabs__nav-next { - height: 40px; - background: #c9c9c9; - - .el-icon { - color: #c61429 - } - } - } - - & > .el-tabs__content { - flex: 1; - - & > .el-tab-pane { - height: 100%; - } - - .section-canvas { - height: 100%; - display: flex; - flex-direction: column; - - .section-toolbar { - flex-shrink: 0; - height: 30px; - display: flex; - align-items: center; - - .el-button { - margin-left: 5px; - } - - .section-toolbar-line { - width: 1px; - height: 16px; - background: #dcdcdc; - margin: 0 5px; - } - - &.section-bottom-toolbar { - justify-content: space-between; - - .section-toolbar-left { - display: flex; - align-items: center; - } - - .section-toolbar-right { - display: flex; - flex-direction: row; - align-items: center; - - .infor { - background: #000; - margin: 0 5px; - color: #fff; - font-size: 12px; - min-width: 120px; - text-align: center; - padding: 3px 5px; - } - } - } - } - - .section-content { - flex: 1; - background: #e0e0e0; - display: flex; - overflow: hidden; - - & > .canvas-container { - flex: 1; - position: relative; - } - } - } - } - } - } - } -} - -.el-popper .el-divider--horizontal { - margin: 5px 0; - border-color: #656668 -} - -.model-3d-view-wrap.el-dialog.resize-dialog { - padding: 0 !important; - - .el-dialog__footer { - //padding-top:8px; - display: none; - } - - .el-dialog__header { - display: flex; - align-items: center; - padding: 8px 0 8px 8px; - } -} \ No newline at end of file diff --git a/src/views/ModelMain.vue b/src/views/ModelMain.vue deleted file mode 100644 index ec338ba..0000000 --- a/src/views/ModelMain.vue +++ /dev/null @@ -1,305 +0,0 @@ - - \ No newline at end of file diff --git a/src/views/ModelMainInit.ts b/src/views/ModelMainInit.ts deleted file mode 100644 index 8b629e5..0000000 --- a/src/views/ModelMainInit.ts +++ /dev/null @@ -1,67 +0,0 @@ -import _ from 'lodash' -import hotkeys from 'hotkeys-js' -import AlarmMeta from '@/designer/viewWidgets/alarm/AlarmMeta' -import LoggerMeta from '@/designer/viewWidgets/logger/LoggerMeta' -import ModeltreeMeta from '@/designer/viewWidgets/modeltree/ModeltreeMeta' -import MonitorMeta from '@/designer/viewWidgets/monitor/MonitorMeta' -import PropertyMeta from '@/designer/viewWidgets/property/PropertyMeta' -import ScriptMeta from '@/designer/viewWidgets/script/ScriptMeta' -import TaskMeta from '@/designer/viewWidgets/task/TaskMeta' -import ToolboxMeta from '@/designer/viewWidgets/toolbox/ToolboxMeta' - -import FileMenu from '@/designer/menus/FileMenu.ts' -import EditMenu from '@/designer/menus/EditMenu.ts' -import ToolsMenu from '@/designer/menus/Tools.ts' -import Model3DView from '@/designer/menus/Model3DView.ts' -import { forEachMenu } from '@/runtime/DefineMenu.ts' -import { normalizeShortKey } from '@/utils/webutils.ts' -import WorldModel from '@/model/WorldModel.ts' - -/** - * 初始化模型编辑器的基础控件 - */ -export function ModelMainInit() { - AlarmMeta.install() - LoggerMeta.install() - ModeltreeMeta.install() - MonitorMeta.install() - PropertyMeta.install() - ScriptMeta.install() - TaskMeta.install() - ToolboxMeta.install() - - FileMenu.install() - EditMenu.install() - ToolsMenu.install() - Model3DView.install() - - const worldModel = new WorldModel() - window['worldModel'] = worldModel -} - -export function ModelMainMounted() { - - forEachMenu((menu) => { - if (typeof menu.click === 'function') { - - const shortKey = normalizeShortKey(menu.tip) - if (shortKey) { - menu.tip = shortKey - hotkeys(shortKey, (event) => { - event.preventDefault() - menu.click() - }) - // console.log('hotkeys', menu.tip, menu.click) - } - } - }) - - return worldModel.init().then(() => { - worldModel.open() - }) -} - -export function ModelMainUnmounted() { - // 移除所有的热键绑定 unbind all - hotkeys.unbind() -} \ No newline at end of file