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 0000000..6a58f48 Binary files /dev/null and b/src/assets/images/conveyor/shapes/logo.png differ diff --git a/src/designer/model3DView/Model3DView.vue b/src/components/Model3DView.vue similarity index 99% rename from src/designer/model3DView/Model3DView.vue rename to src/components/Model3DView.vue index 4cd7960..e5b7ecf 100644 --- a/src/designer/model3DView/Model3DView.vue +++ b/src/components/Model3DView.vue @@ -88,7 +88,7 @@ import { renderIcon } from '@/utils/webutils.js' import textureUrl from '@/assets/images/conveyor/shapes/RibSideSkirtThumbnail.jpg' import moveUrl from '@/assets/images/conveyor/shapes/move.svg' import arrowRightUrl from '@/assets/images/conveyor/shapes/arrow-right.svg' -// import rackUrl from '@/assets/images/conveyor/shapes/Rack.png' +import rackUrl from '@/assets/images/conveyor/shapes/Rack.png' // DOM refs const canvasContainer = ref(null) diff --git a/src/designer/Constract.ts b/src/core/Constract.ts similarity index 100% rename from src/designer/Constract.ts rename to src/core/Constract.ts diff --git a/src/model/ModelUtils.ts b/src/core/ModelUtils.ts similarity index 89% rename from src/model/ModelUtils.ts rename to src/core/ModelUtils.ts index 31c01c4..4ea6e35 100644 --- a/src/model/ModelUtils.ts +++ b/src/core/ModelUtils.ts @@ -1,44 +1,44 @@ import * as THREE from 'three' import type { ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine.ts' -import type { ItemJson } from '@/model/WorldModelType.ts' import { getAllItemTypes, getItemTypeByName } from '@/model/itemType/ItemTypeDefine.ts' -import type Viewport from '@/designer/Viewport.ts' +import type Viewport from '@/core/engine/Viewport' import { computeBoundsTree, disposeBoundsTree } from 'three-mesh-bvh' import { Vector2 } from 'three/src/math/Vector2' import type Toolbox from '@/model/itemType/Toolbox.ts' export function deletePointByKeyboard() { - const viewport: Viewport = window['viewport'] - if (!viewport) { - system.msg('没有找到当前视图') - return - } - - // 按下 Delete 键,删除当前选中的点 - if (!viewport.state.selectedObject) { - system.msg('没有选中任何点') - return - } - - const selectedObject = viewport.state.selectedObject - if (!(selectedObject instanceof THREE.Object3D)) { - system.msg('选中的对象不是有效的点') - return - } - - if (!selectedObject.userData?.type) { - system.msg('选中的对象没有类型信息') - return - } - - const toolbox: Toolbox = viewport.toolbox[selectedObject.userData.type] - if (!toolbox) { - system.msg('没有找到对应的工具箱') - return - } - - viewport.state.cursorMode = 'normal' - toolbox.deletePoint(selectedObject) + system.msg('Delete not impleted yet') + // const viewport: Viewport = window['viewport'] + // if (!viewport) { + // system.msg('没有找到当前视图') + // return + // } + // + // // 按下 Delete 键,删除当前选中的点 + // if (!viewport.state.selectedObject) { + // system.msg('没有选中任何点') + // return + // } + // + // const selectedObject = viewport.state.selectedObject + // if (!(selectedObject instanceof THREE.Object3D)) { + // system.msg('选中的对象不是有效的点') + // return + // } + // + // if (!selectedObject.userData?.type) { + // system.msg('选中的对象没有类型信息') + // return + // } + // + // const toolbox: Toolbox = viewport.toolbox[selectedObject.userData.type] + // if (!toolbox) { + // system.msg('没有找到对应的工具箱') + // return + // } + // + // viewport.state.cursorMode = 'normal' + // toolbox.deletePoint(selectedObject) } export function escByKeyboard() { 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/designer/model2DEditor/DragControls.js b/src/core/controls/DragControls.js similarity index 98% rename from src/designer/model2DEditor/DragControls.js rename to src/core/controls/DragControls.js index e8fe318..a4a7c74 100644 --- a/src/designer/model2DEditor/DragControls.js +++ b/src/core/controls/DragControls.js @@ -6,7 +6,7 @@ import { Vector2, Vector3 } from 'three' -import { calcPositionUseSnap } from '@/model/ModelUtils.js' +import { calcPositionUseSnap } from '@/core/ModelUtils.js' const _plane = new Plane() const _raycaster = new Raycaster() diff --git a/src/designer/model2DEditor/EsDragControls.ts b/src/core/controls/EsDragControls.ts similarity index 95% rename from src/designer/model2DEditor/EsDragControls.ts rename to src/core/controls/EsDragControls.ts index 9b129c0..01b718f 100644 --- a/src/designer/model2DEditor/EsDragControls.ts +++ b/src/core/controls/EsDragControls.ts @@ -1,9 +1,8 @@ -import { DragControls } from './DragControls.js' import * as THREE from 'three' -import type Viewport from '@/designer/Viewport.ts' -import Constract from '@/designer/Constract.ts' -import { getItemTypeByName } from '@/model/itemType/ItemTypeDefine.ts' -import type { ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine.ts' +import { DragControls } from './DragControls.js' +import type Viewport from '@/core/engine/Viewport.ts' +import { getItemTypeByName } from '@/model/itemType/ItemTypeDefine' +import type { ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine' import { markRaw } from 'vue' import EventBus from '@/runtime/EventBus' @@ -96,7 +95,7 @@ export default class EsDragControls { // 拖拽中 drag(e) { - EventBus.$emit('objectChanged', { + EventBus.dispatch('objectChanged', { viewport: this, object: e.object }) @@ -121,7 +120,7 @@ export default class EsDragControls { } if (e.object.userData.selectable) { this.viewport.state.selectedObject = markRaw(e.object) - EventBus.$emit('objectChanged', { + EventBus.dispatch('objectChanged', { viewport: this, object: e.object }) diff --git a/src/designer/model2DEditor/tools/ITool.ts b/src/core/controls/IControls.ts similarity index 65% rename from src/designer/model2DEditor/tools/ITool.ts rename to src/core/controls/IControls.ts index 20806e0..2476375 100644 --- a/src/designer/model2DEditor/tools/ITool.ts +++ b/src/core/controls/IControls.ts @@ -1,4 +1,4 @@ -export interface ITool { +export default interface IControls { init(viewport: any): void destory(): void diff --git a/src/designer/model2DEditor/tools/MouseMoveInspect.ts b/src/core/controls/MouseMoveInspect.ts similarity index 91% rename from src/designer/model2DEditor/tools/MouseMoveInspect.ts rename to src/core/controls/MouseMoveInspect.ts index fc539be..27ea283 100644 --- a/src/designer/model2DEditor/tools/MouseMoveInspect.ts +++ b/src/core/controls/MouseMoveInspect.ts @@ -1,5 +1,5 @@ -import type Viewport from '@/designer/Viewport.ts' -import type { ITool } from '@/designer/model2DEditor/tools/ITool.ts' +import type Viewport from '@/core/engine/Viewport' +import type IControls from './IControls' import * as THREE from 'three' let pmFn, otFn, lvFn @@ -7,7 +7,7 @@ let pmFn, otFn, lvFn /** * 鼠标移动时,将鼠标位置的坐标转换为设计图上的坐标,并设置到 designer.mousePos 属性中 */ -export default class MouseMoveInspect implements ITool { +export default class MouseMoveInspect implements IControls { viewport: Viewport canvas: HTMLCanvasElement diff --git a/src/designer/model2DEditor/tools/SelectInspect.ts b/src/core/controls/SelectInspect.ts similarity index 96% rename from src/designer/model2DEditor/tools/SelectInspect.ts rename to src/core/controls/SelectInspect.ts index e115b49..9024857 100644 --- a/src/designer/model2DEditor/tools/SelectInspect.ts +++ b/src/core/controls/SelectInspect.ts @@ -1,7 +1,7 @@ import * as THREE from 'three' -import type { ITool } from './ITool.ts' +import type IControls from './IControls' import { watch } from 'vue' -import type Viewport from '@/designer/Viewport.ts' +import type Viewport from '@/core/engine/Viewport' import { Line2 } from 'three/examples/jsm/lines/Line2.js' import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js' import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js' @@ -13,7 +13,7 @@ let pdFn, pmFn, puFn /** * 选择工具,用于在设计器中显示选中对象的包围盒 */ -export default class SelectInspect implements ITool { +export default class SelectInspect implements IControls { viewport: Viewport /** * 线框材质,用于显示选中对象的包围盒 @@ -64,7 +64,7 @@ export default class SelectInspect implements ITool { this.canvas.addEventListener('pointerup', puFn) this.viewport.watchList.push(watch(() => this.viewport.state.selectedObject, this.updateSelectionBox.bind(this))) - EventBus.$on('objectChanged', (data) => { + EventBus.on('objectChanged', (data) => { this.updateSelectionBox(this.viewport.state.selectedObject) }) } 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/designer/Viewport.ts b/src/core/engine/Viewport.ts similarity index 74% rename from src/designer/Viewport.ts rename to src/core/engine/Viewport.ts index c38e0bd..1b9571b 100644 --- a/src/designer/Viewport.ts +++ b/src/core/engine/Viewport.ts @@ -1,22 +1,24 @@ import _ from 'lodash' import * as THREE from 'three' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' -import EsDragControls from './model2DEditor/EsDragControls' import Stats from 'three/examples/jsm/libs/stats.module' -import type WorldModel from '@/model/WorldModel.ts' +import type WorldModel from '../manager/WorldModel' import $ from 'jquery' import { reactive, watch } from 'vue' -import type { ITool } from '@/designer/model2DEditor/tools/ITool.ts' +import type IControls from '../controls/IControls' import { CSS3DRenderer } from 'three/examples/jsm/renderers/CSS3DRenderer' import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer' import { getAllItemTypes, type ItemTypeMeta } from '@/model/itemType/ItemTypeDefine.ts' import type { ItemTypeDefineOption } from '@/model/itemType/ItemTypeDefine.ts' -import type Toolbox from '@/model/itemType/Toolbox.ts' -import { calcPositionUseSnap } from '@/model/ModelUtils.ts' -import SelectInspect from '@/designer/model2DEditor/tools/SelectInspect.ts' -import MouseMoveInspect from '@/designer/model2DEditor/tools/MouseMoveInspect.ts' -import type { CursorMode, VDataItem } from '@/types/Types' +import SceneHelp from './SceneHelp' +import SelectInspect from '../controls/SelectInspect' +import MouseMoveInspect from '../controls/MouseMoveInspect' +import EsDragControls from '../controls/EsDragControls' +import EntityManager from '../manager/EntityManager' +import InteractionManager from '@/core/manager/InteractionManager' +import { calcPositionUseSnap } from '@/core/ModelUtils' +import StateManager from '@/core/manager/StateManager.ts' /** * 编辑器对象 @@ -24,28 +26,28 @@ import type { CursorMode, VDataItem } from '@/types/Types' */ export default class Viewport { viewerDom: HTMLElement - scene: THREE.Scene camera: THREE.OrthographicCamera renderer: THREE.WebGLRenderer - axesHelper: THREE.GridHelper - gridHelper: THREE.GridHelper statsControls: Stats controls: OrbitControls - worldModel: WorldModel raycaster: THREE.Raycaster dragControl: any // EsDragControls animationFrameId: any = null + scene: SceneHelp - //搭配 state.cursorMode = xxx 之后, currentTool.start(第一个参数) 使用 - toolStartObject: THREE.Object3D | null = null - currentTool: Toolbox | null = null - tools: ITool[] = [ + tools: IControls[] = [ new MouseMoveInspect(), new SelectInspect() ] - toolbox: Record = {} - objectMap: Map = new Map() + // 状态管理器 + stateManager: StateManager + + // 实体管理器 + entityManager = new EntityManager() + + // 交互管理器 + interactionManager = new InteractionManager() beginSync() { } @@ -62,6 +64,18 @@ export default class Viewport { endSync() { } + get worldModel(): WorldModel { + return this.scene.worldModel + } + + get axesHelper(): THREE.GridHelper { + return this.scene.axesHelper + } + + get gridHelper(): THREE.GridHelper { + return this.scene.gridHelper + } + /** * 监听窗口大小变化 */ @@ -77,7 +91,6 @@ export default class Viewport { //@ts-ignore state: ViewportState = reactive({ - currentFloor: '', isReady: false, cursorMode: 'normal', selectedObject: null, @@ -91,23 +104,22 @@ export default class Viewport { } }) - constructor(worldModel: WorldModel) { - this.worldModel = worldModel + constructor(sceneHelp: SceneHelp, viewerDom: HTMLElement) { + this.scene = sceneHelp + this.viewerDom = viewerDom } /** * 初始化 THREE 渲染器 */ - initThree(viewerDom: HTMLElement, floor: string) { - console.log('viewport on floor', floor) - this.state.currentFloor = floor - this.viewerDom = viewerDom - const rect = viewerDom.getBoundingClientRect() - this.worldModel.registerViewport(this) + initThree(option: InitThreeOption) { + console.log('viewport on catelogCode: ' + this.scene.catalogCode) + const viewerDom = this.viewerDom - // 场景 - const scene = this.worldModel.getSceneByFloor(this, this.state.currentFloor) - this.scene = scene + // 初始化各种管理器 + this.entityManager.init(this) + this.interactionManager.init(this) + this.stateManager = new StateManager(option.stateManagerId, this) // 渲染器 const renderer = new THREE.WebGLRenderer({ @@ -163,47 +175,6 @@ export default class Viewport { // 注册拖拽组件 this.dragControl = new EsDragControls(this) - // 辅助线 - const gridOption = this.worldModel.gridOption - const axesHelper = new THREE.GridHelper(gridOption.axesSize, gridOption.axesDivisions) - axesHelper.material.color.setHex(gridOption.axesColor) - axesHelper.material.linewidth = 2 - axesHelper.material.opacity = gridOption.gridOpacity - axesHelper.material.transparent = true - if (!gridOption.axesEnabled) { - axesHelper.visible = false - } - - // @ts-ignore - axesHelper.material.vertexColors = false - this.axesHelper = axesHelper - this.scene.add(this.axesHelper) - - const gridHelper = new THREE.GridHelper(gridOption.gridSize, gridOption.gridDivisions) - gridHelper.material.color.setHex(gridOption.gridColor) - gridHelper.material.opacity = gridOption.gridOpacity - gridHelper.material.transparent = true - // @ts-ignore - gridHelper.material.vertexColors = false - if (!gridOption.gridEnabled) { - gridHelper.visible = false - } - - this.gridHelper = gridHelper - this.scene.add(this.gridHelper) - - // 光照 - const ambientLight = new THREE.AmbientLight(0xffffff, 0.8) - scene.add(ambientLight) - - // const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5) - // directionalLight.position.set(5, 5, 5).multiplyScalar(3) - // directionalLight.castShadow = true - // scene.add(directionalLight) - // - // const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1) - // scene.add(hemisphereLight) - // 性能监控 const statsControls = new Stats() this.statsControls = statsControls @@ -223,34 +194,6 @@ export default class Viewport { } this.updateGridVisibility() })) - this.watchList.push(watch(() => this.state.cursorMode, (newVal: CursorMode) => { - if (!this.state.isReady) { - return - } - if (this.currentTool) { - this.currentTool.stop() - this.currentTool = null - } - if (newVal === 'normal' || !newVal) { - this.dragControl.dragControls.enabled = true - return - } - - const currentTool = this.toolbox[newVal] - if (currentTool) { - // 选择标尺工具 - this.currentTool = currentTool - this.dragControl.dragControls.enabled = false - - } else { - system.showErrorDialog(`当前鼠标模式 ${newVal} 不支持`) - } - - if (this.currentTool) { - this.currentTool.start(this.toolStartObject) - this.toolStartObject = null - } - })) // 监听窗口大小变化 if (this.resizeObserver) { @@ -332,8 +275,10 @@ export default class Viewport { this.animationFrameId = requestAnimationFrame(this.animate.bind(this)) this.renderView() - this.offset -= 0.002 - window['lineMaterial'].dashOffset = this.offset + if(window['lineMaterial']) { + this.offset -= 0.002 + window['lineMaterial'].dashOffset = this.offset + } } /** @@ -341,10 +286,10 @@ export default class Viewport { */ renderView() { this.statsControls?.update() - this.renderer?.render(this.scene, this.camera) + this.renderer?.render(this.scene.scene, this.camera) - this.css2DRenderer.render(this.scene, this.camera) - this.css3DRenderer.render(this.scene, this.camera) + this.css2DRenderer.render(this.scene.scene, this.camera) + this.css3DRenderer.render(this.scene.scene, this.camera) } /** @@ -377,7 +322,7 @@ export default class Viewport { handleResize(entries: any) { for (let entry of entries) { // entry.contentRect包含了元素的尺寸信息 - console.log('Element size changed:', entry.contentRect) + // console.log('Element size changed:', entry.contentRect) const width = entry.contentRect.width const height = entry.contentRect.height @@ -452,8 +397,6 @@ export default class Viewport { this.resizeObserver = undefined } - this.worldModel.unregisterViewport(this) - if (this.statsControls) { this.statsControls.dom.remove() } 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/designer/StateManager.ts b/src/core/manager/StateManager.ts similarity index 92% rename from src/designer/StateManager.ts rename to src/core/manager/StateManager.ts index 91f3eeb..e7044c3 100644 --- a/src/designer/StateManager.ts +++ b/src/core/manager/StateManager.ts @@ -1,15 +1,13 @@ import _ from 'lodash' import localforage from 'localforage' -import type Viewport from '@/designer/Viewport.ts' +import type Viewport from '@/core/engine/Viewport.ts' import { markRaw, reactive, ref } from 'vue' -import type { VData, VDataItem } from '@/types/Types' /** - * THREE.js 场景状态管理器. - * 从数据结构还原 threejs 画布的一种结构 + * 一种管理 地图数据状态的管理器, 他能够对数据进行增删改查,并且能够进行撤销、重做等操作 * 1. 管理场景数据的读取、保存, 以及临时保存、临时读取等功能 * 2. 管理撤销、重做功能 - * 3. 用户侧会通过如下步骤修改数据 + * 3. 各种 Interaction 交互控制组件通过如下步骤修改数据 * - 1. 调用 beginUserWrite 开始修改数据 * - 2. 直接修改 vdata 数据 * - 3. 调用 endUserWrite 完成数据修改 @@ -45,29 +43,29 @@ import type { VData, VDataItem } from '@/types/Types' */ export default class StateManager { /** - * 是否发生了变化,通知外部是否需要保存数据 + * 唯一场景标识符, 用于做临时存储的 key */ - isChanged = ref(false) + readonly id: string /** - * 是否正在加载数据,通知外部是否需要等待加载完成 + * 视口对象, 用于获取、同步当前场景的状态 */ - isLoading = ref(false) + readonly viewport: Viewport /** - * 当前场景数据 + * 是否发生了变化,通知外部是否需要保存数据 */ - vdata: VData = { items: [], isChanged: false } + readonly isChanged = ref(false) /** - * 唯一场景标识符, 用于做临时存储的 key + * 是否正在加载数据,通知外部是否需要等待加载完成 */ - id: string + readonly isLoading = ref(false) /** - * 视口对象, 用于获取、同步当前场景的状态 + * 当前场景数据 */ - viewport: Viewport + vdata: VData /** * 使用循环缓冲区存储历史记录 @@ -102,7 +100,8 @@ export default class StateManager { */ constructor(id: string, viewport: Viewport, bufferSize = 50) { this.id = id - this.viewport = markRaw(viewport) + this.viewport = viewport + this.historyBufferSize = bufferSize // 初始化固定大小的历史缓冲区 this.historySteps = new Array(this.maxHistorySteps).fill(null) @@ -240,7 +239,7 @@ export default class StateManager { /** * 从外部加载数据 */ - async load(items: VDataItem[]) { + async load(data: VData) { this.isLoading.value = true this.historySteps = new Array(this.maxHistorySteps).fill(null) this.historyIndex = -1 @@ -250,7 +249,12 @@ export default class StateManager { this.stopAutoSave() // 直接替换数组引用(避免响应式开销) - this.vdata.items = items + this.vdata = { + id: this.id, + items: data.items, + isChanged: false, + catalog: data.catalog + } this.fullSync() // 同步到视口 // 初始状态作为第一步 @@ -264,7 +268,7 @@ export default class StateManager { await this.saveToLocalstore() this.pendingChanges = false - console.log('[StateManager] 加载完成,共 ', items.length, '个对象') + console.log('[StateManager] 加载完成,共 ', data.items.length, '个对象') } finally { this.isLoading.value = false @@ -424,10 +428,16 @@ export default class StateManager { // } // } + /** + * 保存到本地存储 浏览器indexDb(防止数据丢失) + */ async saveToLocalstore() { await localforage.setItem(`scene-tmp-${this.id}`, this.vdata) } + /** + * 从本地存储还原数据 + */ async loadFromLocalstore() { try { this.isLoading.value = true @@ -480,21 +490,21 @@ export default class StateManager { * 启动自动保存定时器 */ startAutoSave() { - if (this.autoSaveInterval) return - - this.autoSaveInterval = window.setInterval(() => { - this.autoSaveIfNeeded() - }, this.autoSaveIntervalMs) + // if (this.autoSaveInterval) return + // + // this.autoSaveInterval = window.setInterval(() => { + // this.autoSaveIfNeeded() + // }, this.autoSaveIntervalMs) } /** * 停止自动保存定时器 */ stopAutoSave() { - if (this.autoSaveInterval) { - clearInterval(this.autoSaveInterval) - this.autoSaveInterval = null - } + // if (this.autoSaveInterval) { + // clearInterval(this.autoSaveInterval) + // this.autoSaveInterval = null + // } } /** @@ -519,11 +529,9 @@ export default class StateManager { */ destroy() { this.stopAutoSave() - this.removeLocalstore().catch(console.error) // 清理引用 - this.viewport = null as any - this.vdata.items = [] - this.historySteps = [] + delete this.vdata + delete this.historySteps } /** 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/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/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/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/views/ModelMain.less b/src/editor/ModelMain.less similarity index 100% rename from src/views/ModelMain.less rename to src/editor/ModelMain.less diff --git a/src/views/ModelMain.vue b/src/editor/ModelMain.vue similarity index 96% rename from src/views/ModelMain.vue rename to src/editor/ModelMain.vue index ec338ba..eb87d2a 100644 --- a/src/views/ModelMain.vue +++ b/src/editor/ModelMain.vue @@ -15,7 +15,7 @@ -
+