diff --git a/README.md b/README.md index d509ca4..d168ae6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,94 @@ -# yvan-rcs-web +# 系统概述 + +凯乐士物流控制中心(Galaxis logistics control center) + +作为全球领先的智慧物流中枢平台,凯乐士创新性地集成物流建模、物联控制、数字孪生与智能调度四大核心模块,为企业提供从数字建模到物理执行的全链路闭环管理,构建新一代柔性化智能仓储体系。 + +## 平台架构 + +- 数据建模 ModelEditor + + - 设备数字孪生:支持8大类仓储设备建模 + + - 存储体系:动态配置暂存区/地堆货架/ASRS立库/穿梭式密集库等多元存储方案 + - 执行矩阵:堆垛机/四向穿梭车/智能提升机/机械臂集群等智能设备组态 + - 运输网络:模块化集成输送线/智能AGV/无人叉车等多形态运输体系 + - 容器管理:适配原箱/托盘/周转箱等12种工业级载具规格 + - 流程可视化编排:通过拖拽式界面实现仓储动线设计、工序流程配置及工艺参数优化 +- 物联控制 Monitor + + - 多协议兼容:支持OPC UA/Modbus/MQTT等15+工业协议 + - 设备健康管理:实时诊断200+设备运行指标 + - 边缘计算能力:部署设备级智能决策节点 +- 任务调度中心 + + - 任务调度:业务工单 → 调度指令 → 设备作业 → 控制报文(四层精准拆解) + - 对接多种调度系统: + + - RCS机器人调度系统 + - MFC多层穿梭车控制系统 + - PES穿梭板调度系统 + - WCS设备控制系统智能联动 + - WMS仓储业务管理系统 +- 仿真优化中枢 + + - 数字孪生预演:支持3D可视化仓储布局仿真 + - 智能推演系统: + » 吞吐量压力测试 » 设备配置验证 » 异常工况模拟 + - ROI分析引擎:精准测算产能提升率/空间利用率/设备投入产出比 + + +## 核心价值 + +- 规避试错成本:通过数字仿真降低85%的实体测试投入 +- 提升运营效率:智能调度算法使设备协同效率提升40%+ +- 强化系统韧性:实时物联监控降低70%非计划停机风险 +- 加速部署周期:模块化架构实现新项目部署周期缩短60% + + +## **核心特点** + +**1️⃣ 智能建模编辑器——柔性化仓储设计中枢** + +- **三维可视化布局**:支持拖拽式设备排布与动线规划,实时渲染仓储空间热力图 +- **动态属性配置**:可调整200+设备参数(速度/载重/能耗等),支持参数联动优化 +- **批量工程管理**:一键同步调整多设备组态,支持设备模板库快速复用 +- **跨平台兼容**:导入/导出FBX/STEP/GLTF等8种工业模型格式,兼容AutoCAD/BIM数据 + +**2️⃣ 全景监控体系——全链路数字化透视** + +- **过程回溯引擎**: + ▸ 设备日志回放 + ▸ 任务执行路径追踪 + ▸ 报文级通信过程可视化 +- **智能诊断矩阵**: + » 实时性能看板:监测设备OEE/任务吞吐量/系统响应延迟等18项核心指标 + » 调试沙盒环境:支持API报文模拟注入与响应解析 + » 日志智能检索:基于自然语言的日志关联分析 +- **可配置仪表盘**:自定义监控看板,支持多维度数据钻取与预警阈值设置 + +**3️⃣ 智能仿真系统——风险预判与决策沙盒** + +- **数字孪生沙盒**: + ▶ 模拟真实设备通信报文(支持Modbus-TCP/Profinet等协议仿真) + ▶ 冲突场景库:预设200+典型设备干涉模型(路径冲突/资源抢占/流量瓶颈) +- **智能推演引擎**: + » 历史日志回放:基于真实运营数据的仿真复现 + » 随机事件发生器:模拟设备故障/订单波动/紧急插单等34类异常工况 + » 数据扰动测试:注入噪声数据验证系统鲁棒性 +- **仿真报告生成**:自动输出瓶颈分析报告与优化建议(含空间利用率/设备稼动率预测) + +**4️⃣ 自适应调度中枢——动态环境下的智能决策** + +- **冲突感知网络**: + ▸ 实时识别6类设备冲突(路径交叉/任务死锁/资源竞争等) + ▸ 冲突热力图谱:可视化展示高频冲突区域 +- **解决方案工坊**: + » 录制专家处理逻辑,构建冲突解决知识库(支持if-then规则/决策树录制) + » AI自动优化:基于深度强化学习的动态路径规划算法 +- **智能仲裁机制**: + ▶ 优先级动态调整(订单紧急度/设备状态/能耗成本多目标优化) + ▶ 微秒级响应:复杂冲突场景决策延迟<5秒 ## 文件结构构成 ``` diff --git a/doc/物流模型总体介绍.md b/doc/物流模型总体介绍.md index 249b83c..60ed3b6 100644 --- a/doc/物流模型总体介绍.md +++ b/doc/物流模型总体介绍.md @@ -1,117 +1,320 @@ -# 物流模型总体介绍 -## 基本定义 - -### 物流单元大纲 -- 点 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 = [ +export default { + project_uuid: 'example1', + Tool: { + Group: [], // 分组 + GlobalVariables: [], // 全局变量 + UserCommand: [], // 全局脚本 + Dashboard: [], // 监控面板 + DataTable: [], // 地图自带的数据 + Trigger: [ // 触发器 + { name: 'OnOpen', fn: '' }, // 打开 + { name: 'OnReset', fn: '' }, // 仿真重置 + { name: 'OnStart', fn: '' }, // 开始仿真 + { name: 'OnStop', fn: '' } // 停止仿真 + ], + gridHelper: { // 网格辅助线 + axesEnabled: true, // 是否显示中心轴 + axesSize: 50, // 中心轴长度 + axesDivisions: 2, // 中心轴分割数 + axesColor: 0x000000, // 中心轴颜色 + axesOpacity: 1, // 中心轴透明度 + + gridEnabled: true, // 是否显示网格 + gridSize: 1000, // 网格大小 + gridDivisions: 1000, // 网格分割数 + gridColor: 0x999999, // 网格颜色 + gridOpacity: 0.8, // 网格透明度 + + snapEnabled: true, // 是否启用吸附 + snapDistance: 0.25 // 吸附距离 + } + }, + items: [ { - value: 'F', label: '仓库楼层', - children: [ - { value: '-f1', label: '地下室 (-f1)' }, - { value: 'f1', label: '一楼 (f1)' }, - { value: 'f2', label: '二楼 (f2)' }, - { value: 'OUT', label: '外场 (OUT)' }, - { value: 'fe', label: '楼层电梯 (fe)' } + catalogCode: 'f1', t: 'floor', // 楼层 + items: [ + { + 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'], // 用于 a='ln' 的测量线段, 关联的点对象(uuid) + in: [], // 物流入方向关联的对象(id) + out: [] // 物流出方向关联的对象(id) + } + }, + { + id: 'p2', + t: 'measure', + tf: [[-9.0, 0, 3], [0, 0, 0], [0.25, 0.1, 0.25]], + dt: { + color: '#ff0000', + label: '测量2', + center: ['p1', 'p3', 'p4'] + } + }, + { + id: 'p3', t: 'measure', + tf: [[-5.0, 0, 3], [0, 0, 0], [0.25, 0.1, 0.25]], + dt: { + label: '测量3', + center: ['p2'] + } + }, + { + id: 'p4', + t: 'measure', + tf: [[-9.0, 0, 8], [0, 0, 0], [0.25, 0.1, 0.25]], + dt: { + label: '测量4', + center: ['p2'] + } + } + ] + } + ], + 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)' } ] }, { - value: 'M', label: '密集库区域', - children: [ - { value: 'm1', label: 'M1 (m1)' }, - { value: 'm2', label: 'M2 (m2)' }, - { value: 'm3', label: 'M3 (m3)' }, - { value: 'm4', label: 'M4 (m4)' }, - { value: 'me', label: '提升机 (me)' } + label: '密集库区域', + items: [ + { catalogCode: 'm1', label: 'M1 (m1)' }, + { catalogCode: 'm2', label: 'M2 (m2)' }, + { catalogCode: 'm3', label: 'M3 (m3)' }, + { catalogCode: 'm4', label: 'M4 (m4)' }, + { catalogCode: 'me', label: '提升机 (me)' } ] }, + { + 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)' } + ] + } ] +} +``` + + +### 物流世界模型 + +```ts +/** + * 物流世界模型 + */ +export default class WorldModel { + /** + * 世界模型双向绑定的状态数据 + */ + state: WorldModelState = reactive({ + isOpened: false, // 是否已打开世界模型 + catalogCode: '', // 当前楼层的目录代码 + isDraft: false, // 是否是草稿数据, 如果是草稿数据, 则不需要再从服务器加载数据 + stateManagerId: '', // 当前楼层的状态管理器id, 一般是 项目ID+目录项ID + catalog: [] as Catalog // 世界模型目录 + }) + + /** + * 初始化世界模型 + */ + init() + + /** + * 读取世界地图数据目录 + */ + loadCatalog(data: any) + + /** + * 当楼层发生改变时调用此方法, 将事件派发出去 + */ + onCatalogCodeChanged(catalogCode: string) + + /** + * 从服务器获取当前目录楼层的所有数据 + */ + async getCatalogData(catalogCode: string): Promise> - // 载入某个楼层 - async loadFloor(floorId: string): Promise } ``` -物流控制中心系统,基于 ThreeJS 和 Vue3 开发. +### 状态管理器 StateManager + +```ts +/** +数据状态管理器 +他能够对数据进行载入, 保存草稿, 载入草稿, 数据增删改查,撤销、重做等操作 +最终他会将数据的变化同步到 EntityManager 中 +主要功能包括: +1. 管理场景数据的读取、保存, 以及临时保存、临时读取等功能 +2. 管理撤销、重做功能 +3. 交互控制组件通过如下步骤修改数据 + - 1. 调用 beginStateUpdate 开始修改数据 + - 2. 直接修改 vdata 数据 + - 3. 调用 endStateUpdate 完成数据修改 +4. 内部如果进行了撤销、还原等操作,会通过 syncDataState() 方法将 vdata 数据与 EntityManager 进行同步 +5. 注意,如果正在读取中,需要设置 isLoading = true,外部需要等待加载完成后再进行操作 + +主要难点: +- 单张地图数据量可能超过 10000 个对象, 需要高效的管理数据状态 + */ +export default class StateManager { + /** + * 唯一场景标识符, 用于做临时存储的 key + */ + readonly id: string + + /** + * 实体对象, 用于同步当前场景的状态 + */ + readonly entityManager: EntityManager + + /** + * 是否发生了变化,通知外部是否需要保存数据 + */ + readonly isChanged = ref(false) + + /** + * 是否正在加载数据,通知外部是否需要等待加载完成 + */ + readonly isLoading = ref(false) + + readonly storeKey: string -他的主要作用就是通过Web浏览器, 建立一个自动化立体仓库的物流模型 + /** + * 当前场景数据 + */ + vdata: VData + /** + * 使用循环缓冲区存储历史记录 + */ + private historySteps: HistoryStep[] = [] + private historyIndex = -1 + private maxHistorySteps = 20 -### 单元点 ItemJson + // 变化追踪器 + private readonly changeTracker: DataDiff = { + added: [], + removed: [], + updated: [] + } + + /** + * 数据快照(用于差异计算) + */ + private lastStateDict = new Map() + + /** + * @param id 唯一场景标识符, 用于做临时存储的 key + * @param viewport 视口对象 + * @param maxHistorySteps 最大回撤步数 默认为 20 + */ + constructor(id: string, viewport: Viewport, maxHistorySteps = 20) + + /** + * 开始用户操作(创建数据快照) + */ + beginStateUpdate() + + /** + * 结束用户操作(计算差异并保存) + */ + endStateUpdate(): void + + /** + * 将当前数据 与 entityManager 进行同步, 对比出不同的部分,分别进行更新 + * - 调用 entityManager.beginEntityUpdate() 开始更新 + * - 调用 entityManager.createOrUpdateEntity(vdataItem) 添加场景中新的实体 + * - 调用 entityManager.deleteEntity(id) 删除场景中的实体 + * - 调用 entityManager.endEntityUpdate() 结束更新场景 + */ + syncDataState(diff: DataDiff): void + + /** + * 从外部加载数据 + */ + async load(items: VDataItem[]) + + /** + * 保存数据到外部 + */ + async save(): Promise + + /** + * 撤销 + */ + undo() + + /** + * 重做 + */ + redo() + + /** + * 保存到本地存储 浏览器indexDb(防止数据丢失) + */ + async saveToLocalstore() + + /** + * 从本地存储还原数据 + */ + async loadFromLocalstore() + + /** + * 删除本地存储 + */ + async removeLocalstore() +} +``` + + +### 单元点数据 ItemJson ```ts /** @@ -169,10 +372,12 @@ export interface ItemJson { * S连线(又称逻辑连线), 与其他点之间的无方向性关联, 关系的起点需要在他的 dt.center[] 数组中添加目标点的id, 关系的终点需要在他的 dt.center[] 数组中添加起点的 id */ center?: string[] + /** * A连线(又称物体流动线)的输入, 关系的终点需要在 dt.in[] 数组中添加起点的 id */ in?: string[] + /** * A连线(又称物体流动线)的输出, 关系的起点需要在 dt.out[] 数组中添加目标点的 id */ @@ -187,6 +392,11 @@ export interface ItemJson { * 是否受保护, 不可在图形编辑器中拖拽, 默认 false */ protected?: boolean + + /** + * 在 ThreeJs 中, 这个点应当属于哪个父元素 + */ + parentId?: string /** * 其他自定义数据, 可以存储任何数据 @@ -199,234 +409,152 @@ export interface ItemJson { ## 场景和视窗 + ### 场景 SceneHelp ```ts /** * 场景对象 - * 通常是某个楼层的所有物品和摆放, 每个场景可能会有多个不同的 Viewport 对他进行观察 - * 这是一个成熟的类, 不用对他改造 + * 通常是某个楼层的所有物品, 对应一个 Three.Scene, 每个场景可能会有多个不同的 Viewport 对他进行观察 */ export default class SceneHelp { scene: THREE.Scene - axesHelper: THREE.GridHelper - gridHelper: THREE.GridHelper +} +``` - /** - * 整个仓库的地图模型 - */ - worldModel: WorldModel - /** - * 实体管理器, 所有控制实体都在这里管理 - */ - entityManager: EntityManager +### 视窗 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(floor: string) + constructor(sceneHelp: SceneHelp, viewerDom: HTMLElement) } ``` -### 状态管理器 DataStateManager +### 实体管理器 EntityManager ```ts /** - * 地图数据状态的管理器, 他能够对数据进行增删改查,并且能够进行撤销、重做等操作. - * 所有的修改都应该从这里发起, 多数修改都是从各个物流单元的 interaction 发起 +实体管理器 +缓存所有 数据(ItemJson)和他们的关系, 以及渲染对象 THREE.Object3D +数据更新的流程是: +1. 状态管理器 StateManager 发出数据被修改的请求, 这些请求通常只有 ItemJson 数据 +2. 实体管理器 (EntityManager) 需要计算所有相关的点和线的变化. 在 endUpdate 时需要根据 center / in / out 关系, 计算出: 点的创建 / 点的更新 / 点的删除 / 线的创建 / 线的更新 / 线的删除 +3. 通知对应的渲染器 (Renderer) 进行点和线的创建 / 更新 / 删除 */ -export default class StateManager { - - /** - * 唯一场景标识符, 用于做临时存储的 key - */ - id: string - - /** - * 视口对象, 用于获取、同步当前场景的状态 - */ +export class EntityManager { viewport: Viewport - /** - * 是否发生了变化,通知外部是否需要保存数据 - */ - isChanged = ref(false) - - /** - * 是否正在加载数据,通知外部是否需要等待加载完成 - */ - isLoading = ref(false) + // 所有数据点的实体 + private readonly entities = new Map() - /** - * 当前场景数据 - */ - vdata: VData = { items: [], isChanged: false } - - constructor(id: string, viewport: Viewport) + // 关系索引 + private readonly relationIndex = new Map() - /** - * 开始用户操作(创建数据快照) - */ - beginUpdate() + // 所有 THREEJS "点"对象, 检索值是"点实体"的 id, 值是 THREE.Object3D 数组 + private readonly objects = new Map() - /** - * 结束用户操作(计算差异并保存), 内部会调用 syncDataState 方法, 换起实体管理器 EntityManager - */ - commitUpdate() + // 所有 THREEJS "线"对象, 检索值是"线实体"的 id, 取值方式是 {type}${startId}${endId}, 值是 THREE.Object3D 数组 + private readonly lines = new Map() - /** - * 将当前数据 与 EntityManager 进行同步, 对比出不同的部分,分别进行更新 - * - 调用 viewport.entityManager.beginBatch() 开始更新 - * - 调用 viewport.entityManager.createEntity(vdataItem) 添加场景中新的实体 - * - 调用 viewport.entityManager.updateEntity(vdataItem) 新场景中已存在的实体 - * - 调用 viewport.entityManager.deleteEntity(id) 删除场景中的实体 - * - 调用 viewport.entityManager.commitBatch() 结束更新场景 - */ - syncDataState() + // 差量渲染器 + private readonly diffRenderer = new Map() + // 线差量记录 + private readonly lineDiffs = { + create: new Map(), + update: new Map(), + delete: new Map() + } + + constructor(viewport: Viewport) /** - * 从外部加载数据 + * 批量更新开始 */ - async load(items: VDataItem[]) + beginEntityUpdate(): void /** - * 保存数据到外部 + * 创建或更新一个实体, 这个点的 center[] / in[] / out[] 关联的点, 可能都要对应进行关联 */ - async save(): Promise + createOrUpdateEntity(entity: ItemJson, option: EntityCudOption = {}): void /** - * 撤销 + * 删除实体, 与这个点有关的所有线都要删除, 与这个点有关系的点,都要对应的 center[] / in[] / out[] 都要断绝关系 */ - undo() + deleteEntity(id: string, option: EntityCudOption = {}): void /** - * 重做 + * 批量更新结束, 结束后会触发视窗的渲染 + * 这个方法最重要的是进行连线逻辑的处理 + * - 如果进行了添加, 那么这个点的 center[] / in[] / out[] 关联的点, 都要对应进行关联 + * - 如果进行了删除, 与这个点有关的所有线都要删除, 与这个点有关系的点,都要对应的 center[] / in[] / out[] 都要断绝关系 + * - 如果进行了更新, 如果改了颜色/位置, 则需要在UI上进行对应修改,如果改了关系,需要与关联的节点批量调整 + * 将影响到的所有数据, 都变成一个修改集合, 统一调用对应单元类型渲染器(BaseRenderer)的 createPoint / deletePoint / updatePoint / createLine / updateLine / deleteLine 方法 */ - redo() + endEntityUpdate(): void /** - * 保存到本地存储 浏览器indexDb(防止数据丢失) + * 更新关系关系网, 计算出差值, 可以临时放在 diffRenderer 中, 等待 commitUpdate 时统一处理 */ - async saveToLocalstore() + private updateRelations(entity: ItemJson): void - /** - * 从本地存储还原数据 - */ - async loadFromLocalstore() + private updateReverseRelations(id: string, relatedIds: string[] | undefined, relationType: 'center' | 'in' | 'out'): void /** - * 删除本地存储 + * 删除关系关系网, 计算出差值, 可以临时放在 diffRenderer 中, 等待 commitUpdate 时统一处理 */ - async removeLocalstore() + private removeRelations(id: string): void } -``` - -### 视窗 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) +export class Relation { + center = new Set() + input = new Set() + output = new Set() - /** - * 初始化 THREE 渲染器 - */ - initThree(sceneHelp: SceneHelp, viewerDom: HTMLElement, floor: string) - - /** - * 动画循环 - */ - animate() + add(type: LinkType, id: string): boolean - /** - * 销毁视窗 - */ - destroy() - - /** - * 获取坐标下所有对象 - */ - getIntersects(point: THREE.Vector2): THREE.Object3D[] - - /** - * 获取鼠标所在的 x,y,z 坐标 - * 鼠标坐标是相对于 canvas 元素 (renderer.domElement) 元素的 - */ - getClosestIntersection(e: MouseEvent) + delete(type: LinkType, id: string): boolean } ``` -### 实体管理器 EntityManager +### 交互管理器 InteractionManager ```ts -/** - * 缓存所有实体和他们的关系, 在各个组件的渲染器会调用这个实体管理器, 进行检索 / 关系 / 获取差异等计算 - */ -export class EntityManager { +export default class InteractionManager { + private viewport: Viewport; + /** - * 视窗对象, 所有状态管理器, 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) + private currentTool: BaseInteraction | null = null - // createEntity / updateEntity / deleteEntity 调整完毕之后, 调用这个方法进行收尾 - // 这个方法最重要的是进行连线逻辑的处理 - // - 如果进行了添加, 那么这个点的 center[] / in[] / out[] 关联的点, 都要对应进行关联 - // - 如果进行了删除, 与这个点有关的所有线都要删除, 与这个点有关系的点,都要对应的 center[] / in[] / out[] 都要断绝关系 - // - 如果进行了更新, 如果改了颜色/位置, 则需要在UI上进行对应修改,如果改了关系,需要与关联的节点批量调整 - // 将影响到的所有数据, 都变成一个修改集合, 统一调用对应单元类型渲染器(BaseRenderer)的 createPoint / deletePoint / updatePoint / createLine / updateLine / deleteLine 方法 - // 具体方法就是 viewport.getItemTypeRenderer(itemTypeName) - commitUpdate() + constructor(viewport: Viewport) { + this.viewport = viewport; + } - // 获取实体 - getEntity(id: string): ItemJson | undefined - - // 获取相关实体 - getRelatedEntities(id: string, relationType: 'center' | 'in' | 'out'): ItemJson[] } ``` - ## 物流单元封装 物流单元组件封装, 每个组件类别都会有 @@ -439,74 +567,159 @@ export class EntityManager { 比如测量组件 Measure, 他属于最基础的线类型物流单元. -### 渲染器 renderer +### 基础渲染器 base.renderer ```ts /** - * 基本渲染器基类 - * 定义了点 / 线 该如何渲染到 ThreeJs 场景中, 这里可能会调用 InstancePool 进行渲染 - * 每个物流单元类型, 全局只有一个实例 +基本渲染器基类 +定义了点 / 线如何渲染到 Three.js 场景中. +后期考虑调用 InstancePool 渲染 */ public export default BaseRenderer { - - // 开始更新, 可能暂停动画循环对本渲染器的动画等 - beginUpdate(viewport: Viewport); - - // 创建一个点, 每种物流单元类型不一样, 可能 measure 是创建一个红色方块, 可能 moveline 创建一个菱形, 可能 conveyor 创建一个齿轮 - abstract createPoint(item: ItemJson, option?: RendererCudOption) - // 删除一个点 - abstract deletePoint(id, option?: RendererCudOption); + /** + * 每次 beginUpdate 时记录临时 viewport, endUpdate 之后要马上删除, 因为 BaseRenderer 全局只有一个实例, 而 viewport 有多个 + */ + tempViewport?: Viewport = undefined - // 更新一个点 - abstract updatePoint(item: ItemJson, option?: RendererCudOption); + /** + * 开始更新 + * @param viewport 当前视口 + */ + beginRendererUpdate(viewport: Viewport): void { + this.tempViewport = viewport + } - // 创建一根线, 每种物流单元类型不同 这个方法都不同 - // 可能 measure 对于 in 和 out 忽略, center 就是创造一根 Line2, - // 可能 conveyor 对于 in 和 out 是创造一个 Mesh 并带动画 带纹理背景 带几何形状, center 就是画一个红色的细线 - abstract createLine(start: ItemJson, end: ItemJson, type: 'in' | 'out' | 'center', option?: RendererCudOption) + /** + * 创建一个最基本的点对象, 不用管 item 的 name / id / 位置 / 转换 / 大小 和 userData, 除非有明确定义 + */ + abstract createPointBasic(item: ItemJson, option?: RendererCudOption): THREE.Object3D[] - // 更新一根线 - abstract updateLine(start: ItemJson, end: ItemJson, type: 'in' | 'out' | 'center', option?: RendererCudOption) + /** + * 创建测量线 + */ + abstract createLineBasic(start: ItemJson, end: ItemJson, type: LinkType): THREE.Object3D[] - // 删除一根线 - abstract deleteLine(start: ItemJson, end: ItemJson, option?: RendererCudOption) - - // 结束更新 - abstract endUpdate(viewport: Viewport); -} + /** + * 将对象添加到当前视口的场景中 + */ + appendToScene(...objects: THREE.Object3D[]): void -/** - * 辅助测量工具渲染器 - */ -public export default class MeasureRenderer extends BaseRenderer { - - // 开始更新, 可能暂停动画循环对本渲染器的动画等 - beginUpdate(viewport: Viewport); - - // 创建一个点 - createPoint(item: ItemJson, option?: RendererCudOption) + /** + * 创建一个点 + * @param item 点的定义 + * @param option 渲染选项 + */ + createPoint(item: ItemJson, option?: RendererCudOption): void - // 删除一个点 - deletePoint(id, option?: RendererCudOption); - // 更新一个点 - updatePoint(item: ItemJson, option?: RendererCudOption) + /** + * 删除一个点 + * @param id 点的唯一标识 + * @param option 渲染选项 + */ + deletePoint(id: string, option?: RendererCudOption): void - // 创建一根线 - createLine(start: ItemJson, end: ItemJson, type: 'in' | 'out' | 'center', option?: RendererCudOption) + /** + * 更新一个点 + * @param item 点的定义 + * @param option 渲染选项 + */ + updatePoint(item: ItemJson, option?: RendererCudOption): void - // 更新一根线 - updateLine(start: ItemJson, end: ItemJson, type: 'in' | 'out' | 'center', option?: RendererCudOption) + /** + * 创建一根线 + * @param start 起点 + * @param end 终点 + * @param type 线的类型 + * @param option 渲染选项 + */ + createLine(start: ItemJson, end: ItemJson, type: LinkType, option?: RendererCudOption): void - // 删除一根线 - deleteLine(start: ItemJson, end: ItemJson, option?: RendererCudOption) - - // 结束更新 - endUpdate(viewport: Viewport); + /** + * 更新一根线 + * @param start 起点 + * @param end 终点 + * @param type 线的类型 + * @param option 渲染选项 + */ + updateLine(start: ItemJson, end: ItemJson, type: LinkType, option?: RendererCudOption): void + + /** + * 删除一根线 + * @param start 起点 + * @param end 终点 + * @param option 渲染选项 + */ + deleteLine(start: ItemJson, end: ItemJson, option?: RendererCudOption): void + + /** + * 结束更新 + */ + endRendererUpdate(): void } ``` + +### 测量尺渲染器 measure.renderer + +```ts + +/** + * 辅助测量工具渲染器 + */ +export default class MeasureRenderer extends BaseRenderer { + /** + * 当前测绘内容组, 所有测量点、线、标签都在这个组中. 但不包括临时点、线 + */ + group: THREE.Group + + static GROUP_NAME = 'measure_group' + static LABEL_NAME = 'measure_label' + static POINT_NAME = 'measure_point' + static LINE_NAME = 'measure_line' + + pointMaterial = new THREE.MeshBasicMaterial({ color: 0x303133, transparent: true, opacity: 0.9 }) + + lineMaterial = new LineMaterial({ + color: 0xE63C17, // 主颜色 + linewidth: 2, // 实际可用的线宽 + vertexColors: true, // 启用顶点颜色 + dashed: false, + alphaToCoverage: true + }) + + createLineBasic(start: ItemJson, end: ItemJson, type: LinkType): THREE.Object3D[] { + const geom = new LineGeometry() + const obj = new Line2(geom, this.lineMaterial) + obj.frustumCulled = false + obj.name = MeasureRenderer.LINE_NAME + obj.uuid = getLineId(start.id, end.id, type) + + return [obj] + } + + createPointBasic(item: ItemJson, option?: RendererCudOption): THREE.Object3D[] { + const tt = new THREE.BoxGeometry(1, 1, 1) + const obj = new THREE.Mesh(tt, this.pointMaterial) + obj.name = MeasureRenderer.POINT_NAME + obj.uuid = item.id + + return [obj] + } + + appendToScene(...objects: THREE.Object3D[]) { + if (!this.group) { + this.group = new THREE.Group() + this.group.name = MeasureRenderer.GROUP_NAME + this.tempViewport?.scene.add(this.group) + } + this.group.add(...objects) + } +} +``` + + ### 实例池 InstancePool ```ts @@ -517,7 +730,7 @@ public export default class MeasureRenderer extends BaseRenderer { -### 交互控制器 interaction +### 基础交互控制器 base.interaction ```ts /** @@ -558,6 +771,13 @@ public default class BaseInteraction { clearTemps() } + +``` + + +### 测量尺 交互控制器 measure.interaction + +```ts public default class MeasureInteraction extends BaseInteraction { // 用户在设计器拽动某个点时触发. 这时调整的都是 "虚点" 和 "虚线" dragPointStart(point: THREE.Object3D) @@ -625,14 +845,14 @@ export default class BaseLineEntity { /** * "线"属性 操作代理实体 */ -export class MeasurePoint : BaseItemEntity { +export class MeasurePoint extends BaseItemEntity { ... 各种 get / set / method } /** * "线"属性 操作代理实体 */ -export class MeasureLine: BaseLineEntity { +export class MeasureLine extends BaseLineEntity { ... 各种 get / set / method } ``` @@ -640,7 +860,7 @@ export class MeasureLine: BaseLineEntity { 所有的模型操作都遵循如流程 -DataStateManager -> EntityManager -> xxx.renderer -> InstancePool +StateManager -> EntityManager -> xxx.renderer -> InstancePool 辅助类的操作则是通过交互控制器 @@ -648,8 +868,4 @@ DataStateManager -> EntityManager -> xxx.renderer -> InstancePool interaction 完成临时辅助对象的创建, 辅助线 / 临时单元 是不会被持久化的 -点之间的关系不会非常复杂, 通常是比较稀疏的, 可能一个点最多有6个连线, 绝大部分点 只有1~2个关系连线. - -在大规模制图, 比如单场景中存在 10000 个以上的点, 是否存在性能问题? - -这种封装是否合理, 有什么优化建议? \ No newline at end of file +点之间的关系不会非常复杂, 通常是比较稀疏的, 可能一个点最多有6个连线, 绝大部分点 只有1~2个关系连线. \ No newline at end of file diff --git a/package.json b/package.json index 257ab97..ee3efc7 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,9 @@ "type": "module", "scripts": { "dev": "vite", - "build": "run-p type-check \"build-only {@}\" --", + "build": "vite build", "preview": "vite preview", - "build-only": "vite build", + "build-check": "run-p type-check \"build-only {@}\" --", "type-check": "vue-tsc --build", "format": "prettier --write src/" }, @@ -48,6 +48,7 @@ "@vitejs/plugin-vue": "^5.2.3", "@vitejs/plugin-vue-jsx": "^4.2.0", "@vue/tsconfig": "^0.7.0", + "troika-three-text": "^0.52.4", "mitt": "^3.0.1", "tslib": "2.8.1", "npm-run-all2": "^7.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a14463..f684e79 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -115,7 +115,7 @@ importers: specifier: 2.10.1 version: 2.10.1(three@0.176.0) mitt: - specifier: ^3.0.0 + specifier: ^3.0.1 version: 3.0.1 npm-run-all2: specifier: ^7.0.2 @@ -132,6 +132,9 @@ importers: three-mesh-bvh: specifier: ^0.9.0 version: 0.9.0(three@0.176.0) + troika-three-text: + specifier: ^0.52.4 + version: 0.52.4(three@0.176.0) tslib: specifier: 2.8.1 version: 2.8.1 @@ -555,56 +558,67 @@ packages: resolution: {integrity: sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.41.0': resolution: {integrity: sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.41.0': resolution: {integrity: sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.41.0': resolution: {integrity: sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.41.0': resolution: {integrity: sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.41.0': resolution: {integrity: sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.41.0': resolution: {integrity: sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.41.0': resolution: {integrity: sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.41.0': resolution: {integrity: sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.41.0': resolution: {integrity: sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.41.0': resolution: {integrity: sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.41.0': resolution: {integrity: sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==} @@ -848,6 +862,9 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + birpc@2.3.0: resolution: {integrity: sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==} @@ -1406,6 +1423,10 @@ packages: resolution: {integrity: sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==} engines: {node: ^18.17.0 || >=20.5.0} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} @@ -1515,6 +1536,19 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + troika-three-text@0.52.4: + resolution: {integrity: sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==} + peerDependencies: + three: '>=0.125.0' + + troika-three-utils@0.52.4: + resolution: {integrity: sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==} + peerDependencies: + three: '>=0.125.0' + + troika-worker-utils@0.52.0: + resolution: {integrity: sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -1648,6 +1682,9 @@ packages: typescript: optional: true + webgl-sdf-generator@1.1.1: + resolution: {integrity: sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2373,6 +2410,10 @@ snapshots: balanced-match@1.0.2: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + birpc@2.3.0: {} brace-expansion@2.0.1: @@ -2894,6 +2935,8 @@ snapshots: json-parse-even-better-errors: 4.0.0 npm-normalize-package-bin: 4.0.0 + require-from-string@2.0.2: {} + rfdc@1.4.1: {} rimraf@6.0.1: @@ -3005,6 +3048,20 @@ snapshots: totalist@3.0.1: {} + troika-three-text@0.52.4(three@0.176.0): + dependencies: + bidi-js: 1.0.3 + three: 0.176.0 + troika-three-utils: 0.52.4(three@0.176.0) + troika-worker-utils: 0.52.0 + webgl-sdf-generator: 1.1.1 + + troika-three-utils@0.52.4(three@0.176.0): + dependencies: + three: 0.176.0 + + troika-worker-utils@0.52.0: {} + tslib@2.8.1: {} typescript@5.8.3: {} @@ -3122,6 +3179,8 @@ snapshots: optionalDependencies: typescript: 5.8.3 + webgl-sdf-generator@1.1.1: {} + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/src/core/manager/WorldModel.ts b/src/core/manager/WorldModel.ts index 3e828c9..e9b0e24 100644 --- a/src/core/manager/WorldModel.ts +++ b/src/core/manager/WorldModel.ts @@ -13,7 +13,7 @@ export interface WorldModelState { } /** - * 世界模型 + * 物流世界模型 */ export default class WorldModel { data: any = null @@ -23,10 +23,10 @@ export default class WorldModel { */ state: WorldModelState = reactive({ isOpened: false, // 是否已打开世界模型 - catalogCode: '', - isDraft: false, - stateManagerId: '', // 当前楼层的状态管理器id - catalog: [] as Catalog // 世界模型目录数据 + catalogCode: '', // 当前楼层的目录代码 + isDraft: false, // 是否是草稿数据, 如果是草稿数据, 则不需要再从服务器加载数据 + stateManagerId: '', // 当前楼层的状态管理器id, 一般是 项目ID+目录项ID + catalog: [] as Catalog // 世界模型目录 }) get gridOption(): IGridHelper { @@ -51,6 +51,9 @@ export default class WorldModel { constructor() { } + /** + * 初始化世界模型 + */ init() { // 观察 this.state.catalogCode 的变化, 如果变化就调用 catalogCodeChange 方法 watch(() => this.state.catalogCode, this.onCatalogCodeChanged.bind(this)) @@ -84,7 +87,7 @@ export default class WorldModel { } /** - * 当楼层发生改变时, 将事件派发出去 + * 当楼层发生改变时调用此方法, 将事件派发出去 */ onCatalogCodeChanged(catalogCode: string) { if (this.state.isDraft) { @@ -120,6 +123,9 @@ export default class WorldModel { }) } + /** + * 从服务器获取当前目录楼层的所有数据 + */ async getCatalogData(catalogCode: string): Promise> { if (!this.data || !this.data.items) { return Promise.reject('楼层数据未加载, catalogCode=' + catalogCode) @@ -144,83 +150,4 @@ export default class WorldModel { } return Promise.reject('楼层不存在, catalogCode=' + catalogCode) } - - // 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/example/example1.js b/src/example/example1.js index d270fd9..590b8ac 100644 --- a/src/example/example1.js +++ b/src/example/example1.js @@ -1,33 +1,32 @@ 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: '' } + Group: [], // 分组 + GlobalVariables: [], // 全局变量 + UserCommand: [], // 全局脚本 + 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, + gridHelper: { // 网格辅助线 + axesEnabled: true, // 是否显示中心轴 + axesSize: 50, // 中心轴长度 + axesDivisions: 2, // 中心轴分割数 + axesColor: 0x000000, // 中心轴颜色 + axesOpacity: 1, // 中心轴透明度 - gridEnabled: true, - gridSize: 1000, - gridDivisions: 1000, - gridColor: 0x999999, - gridOpacity: 0.8, + gridEnabled: true, // 是否显示网格 + gridSize: 1000, // 网格大小 + gridDivisions: 1000, // 网格分割数 + gridColor: 0x999999, // 网格颜色 + gridOpacity: 0.8, // 网格透明度 - snapEnabled: true, - snapDistance: 0.25 + snapEnabled: true, // 是否启用吸附 + snapDistance: 0.25 // 吸附距离 } }, items: [ @@ -80,14 +79,14 @@ export default { ] } ], - elevator: [], - wall: [], - pillar: [], - catalog: [ + elevator: [], // 电梯 + wall: [], // 墙体 + pillar: [], // 柱子 + catalog: [ // 目录 { - label: '仓库楼层', + label: '仓库楼层', // 目录分组名 items: [ - { catalogCode: '-f1', label: '地下室 (-f1)' }, + { catalogCode: '-f1', label: '地下室 (-f1)' }, // 目录项 { catalogCode: 'f1', label: '一楼 (f1)' }, { catalogCode: 'f2', label: '二楼 (f2)' }, { catalogCode: 'OUT', label: '外场 (OUT)' }, diff --git a/src/modules/measure/MeasureRenderer.ts b/src/modules/measure/MeasureRenderer.ts index ff2a35d..380a4b5 100644 --- a/src/modules/measure/MeasureRenderer.ts +++ b/src/modules/measure/MeasureRenderer.ts @@ -6,6 +6,7 @@ import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js' import { Line2 } from 'three/examples/jsm/lines/Line2.js' import { numberToString } from '@/utils/webutils.ts' import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer' +import { Text } from 'troika-three-text' /** * 辅助测量工具渲染器 @@ -21,6 +22,8 @@ export default class MeasureRenderer extends BaseRenderer { static POINT_NAME = 'measure_point' static LINE_NAME = 'measure_line' + public useHtmlLabel = false + pointMaterial = new THREE.MeshBasicMaterial({ color: 0x303133, transparent: true, opacity: 0.9 }) lineMaterial = new LineMaterial({ @@ -75,18 +78,27 @@ export default class MeasureRenderer extends BaseRenderer { const p1 = endPoint.position const dist = p0.distanceTo(p1) - const label = `${numberToString(dist)} m` + const label = `长度 ${numberToString(dist)} m` const position = new THREE.Vector3().addVectors(p0, p1).multiplyScalar(0.5) - let labelObj: CSS2DObject | undefined = objects[0].userData.labelObj + let labelObj: Text | CSS2DObject | undefined = objects[0].userData.labelObj if (!labelObj || !labelObj.parent) { labelObj = this.createLabel(label) this.group.add(labelObj) objects[0].userData.labelObj = labelObj } - labelObj.position.set(position.x, position.y, position.z) - labelObj.element.innerHTML = label + labelObj.position.set(position.x, position.y + 0.3, position.z) + + if (this.useHtmlLabel) { + labelObj.element.innerHTML = label + + } else { + // 让文本朝向摄像机 + labelObj.quaternion.copy(this.tempViewport.camera.quaternion) + labelObj.text = label + labelObj.sync() + } } afterDeleteLine(start: ItemJson, end: ItemJson, type: LinkType, option: RendererCudOption, objects: THREE.Object3D[]) { @@ -102,21 +114,42 @@ export default class MeasureRenderer extends BaseRenderer { /** * 创建标签 */ - createLabel(text: string): CSS2DObject { - const div = document.createElement('div') - div.className = 'css2dObjectLabel' - div.innerHTML = text - div.style.padding = '5px 8px' - div.style.color = '#fff' - div.style.fontSize = '14px' - div.style.position = 'absolute' - div.style.backgroundColor = 'rgba(25, 25, 25, 0.3)' - div.style.borderRadius = '12px' - div.style.top = '0px' - div.style.left = '0px' - // div.style.pointerEvents = 'none' //避免HTML元素影响场景的鼠标事件 - const obj = new CSS2DObject(div) - obj.name = MeasureRenderer.LABEL_NAME - return obj + createLabel(text: string): Text | CSS2DObject { + if (this.useHtmlLabel) { + const div = document.createElement('div') + div.className = 'css2dObjectLabel' + div.innerHTML = text + div.style.padding = '5px 8px' + div.style.color = '#fff' + div.style.fontSize = '14px' + div.style.position = 'absolute' + div.style.backgroundColor = 'rgba(25, 25, 25, 0.3)' + div.style.borderRadius = '12px' + div.style.top = '0px' + div.style.left = '0px' + // div.style.pointerEvents = 'none' //避免HTML元素影响场景的鼠标事件 + const obj = new CSS2DObject(div) + obj.name = MeasureRenderer.LABEL_NAME + return obj + + } else { + const label = new Text() + label.text = text + label.fontSize = 0.4 + label.color = '#ff0000' + label.opacity = 0.8 + label.padding = 0.2 + label.anchorX = 'center' + label.anchorY = 'middle' + label.depthOffset = 1 + label.backgroundColor = '#000000' // 黑色背景 + label.backgroundOpacity = 0.6 // 背景半透明 + label.padding = 0.2 // 内边距 + label.material.depthTest = false + label.name = MeasureRenderer.LABEL_NAME + + label.sync() + return label + } } } \ No newline at end of file diff --git a/src/router/index.ts b/src/router/index.ts index f80b5cc..0a0335c 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,6 +1,4 @@ import { createRouter, createWebHashHistory } from 'vue-router' -// import HomeView from '../views/HomeView.vue' -import ModelMain from '../editor/ModelMain.vue' const router = createRouter({ history: createWebHashHistory(import.meta.env.BASE_URL), @@ -8,8 +6,14 @@ const router = createRouter({ { path: '/', name: 'home', + // 自动引导到 /editor + redirect: '/editor' + }, + { + path: '/editor', + name: 'editor', // component: HomeView, - component: ModelMain + component: () => import('../editor/ModelMain.vue') }, { path: '/about',