# ThreeCube **Repository Path**: gypsophlia/three-cube ## Basic Information - **Project Name**: ThreeCube - **Description**: 三体... - **Primary Language**: JavaScript - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2022-02-14 - **Last Updated**: 2023-05-31 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # ThreeCube 开发文档 > 支持多人协同进行 3D 方块世界的搭建 ## 需求分析 完成一款能够根据命令绘制三维图形的支持协同操作的网页应用。 需求点: - 支持通过在线代码或生命式配置方式增加、修改、删除方块. - 支持多人协同编辑,实时展示协同操作反馈。 - 支持视角镜头的变换,控制视角,实时自动化适配场景布置。 - 支持输入命令与命令的自动补全,以实现增删改三维图形的操作。 - 支持查看日志信息,记录同房间内用户的操作记录。 ## 概要设计 前端主要通过`Vue2`的结构化框架完成构建,借助`Three`模块完成三维画布与图形的构建,通过`Websocket`与后端建立长连接。 后端主要通过`Koa`的`Web`框架来完成对数据库`mysql`的`CRUD`操作,借用`WebSocket`实现与前端的全双工通信。 ### 架构图 ![项目架构图](./img/structure.png) - 前后端通过`Websocket`来建立长连接。 - 在`Vue`中通过`EventBus`来存储后端支持的`Api`,挂载到`Vue`上以支持直接调用。 - 同时借助`Vuex`进行状态管理,存储当前用户名称,房间信息等内容,触发全局事件。 - 将三维绘图页面拆分多个组件,包括三维画布组件、日志组件、互动编辑组件、命令输入组件等,以保证结构的优雅和功能的全面。 - 自行设计命令**Command**和命令解析器,支持创建立方体/圆/柱,修改位置/大小/颜色等属性,且支持删除物体。 - 实现交互编辑功能,以非输入命令方式来更新物理属性。 ## 前端设计 ### 路由结构 - `/` 根目录,欢迎页面 - `/home` 房间页面,进入房间绘制三维图形 - `/createHome` 创建房间页面 - `/joinHome` 加入房间页面 - `/exit` 退出房间页面 ### 组件划分 - 三维画布组件 - 初始化相机,场景,渲染器,灯光,控制器等三维环境。 - 初始化三维画布,包含坐标系与地面。 - 添加对`resize`的响应事件,动态重构地面大小,相机位置。 - 视图切换组件 - 四个按钮,分别实现总览视图、主视图、侧视图、俯视图。 - 借助地面大小,动态调整相机位置。 - 保存图片组件 - 鼠标点击可以获取到画布上的像素信息,并以`png`的格式获取像素数据,下载并保存到本地。 - 当画布中没有几何图形时,鼠标点击不会保存图片。 - 操作日志组件 - 一个日志窗口,用以展示当前房间的协同操作日志。 - 将服务器消息进行解析,转换为每条日志。 - 命令窗口组件 - 一个输入框,用以输入命令来创建、更改、删除图形。 - 具备命令自动补全功能,支持常用快捷键`tab`和`enter`。 - 交互编辑组件 - 鼠标点击可拾取画布上的对象,显示出其相关属性:位置,大小,颜色等。 - 鼠标滑动或输入,可以非指令方式更新对象属性。 - 遮罩弹窗组件 - `Toast`消息条,包含`success`,`danger`,`warning`,`info`四种消息类型。 - `Modal`确认框,点击确认可触发回调函数。 - 二者均已挂载到`Vue`原型上。 ### 状态管理 通过`Vuex`完成状态管理,存储以下五种信息,响应以下五个命令: ```js username: "", // 用户昵称 roomid: "", // 房间ID wsState: 0, // websocket状态 commands: [], // 命令集合 offline: false, // 是否处于离线模式 ``` - `COMMIT_COMMAND` - 解析命令并存储,提交至后端服务器。 - `CHANGE_WS_STATE` - 在进入,退出房间时,改变`Websocket`状态 - `SET_USERNAME` - 设置并本地存储用户名 - `SET_ROOMID` - 设计并本地存储房间号 - `SET_OFFLINE` - 设置本地是否处于离线模式 ### 功能列表 #### 三维解析器 > 策略模式完成命令的匹配,责任链模式来挨个执行函数直至成功。 三维解析器采取策略模式和责任链模式来架构。 命令设计结构主要包含三部分: $$ Command = Action + SubAction + RawParams $$ - `action` 为主操作符,代表创建或修改或删除,可选值`['add','alter','delete']`。 - `subAction` 为次操作符,代表具体创建何物或修改何属性或删除何物,可选值`['cube','ball','cylinder','translation','rotate','color','scale','object']` - `rawParams`为粗糙的参数数据,可选值见`/ThreeParser/handlers.js`中的函数参数。 针对一个具体的命令,解析步骤如下: - 解析出主操作符和次操作符,粗糙数据以逗号分割。 - 可执行对应的函数,且将粗糙参数将作为参数传入函数中。 - 验证参数的合法性:个数,类型,范围等。 - 操作对应三维图形 - 在指定位置创建三维图形 - 获取对应三维图形修改图形属性 - 删除对应三维图形 - 将三维图形添加至`cube`中作为包裹,以确认坐标范围。 - 如更改属性或删除图形,借助`eventBus`重新渲染画布。 - 返回该三维图形。 #### 日志解析器 日志解析器采取策略模式和责任链模式来架构。 日志数据结构主要设计为如下二类: ```js const roomMsg = { type: "room", time: "2022/02/24 22:00", // 消息产生的时间 user: "username", // 进入或离开房间的用户名 isJoin: true, // 是加入还是离开 }; const commandMsg = { type: "command", time: "2022/02/24 22:00", // 消息产生的时间 user: "username", // 执行命令的用户名 content: "add cube 1,1,1", // 命令内容 }; ``` 根据`type`判断日志类型,解析流程如下: - 若为`room`类型,则生成用户进入与离开房间的`Html`字符串 - 若为`command`类型 - 根据类似的命令解析步骤,解析出主操作符,次操作符,粗糙参数。 - 根据处理模型来触发对应函数,返回对应的`Html`字符串 - 若为其它类型,则产生错误。 #### 交互编辑 交互编辑的设计结构参照三维画布组件结构搭建,根据射线拾取来拾取某三维对象,借用了`dat.gui`库完成图形参数展示与编辑,采取策略模式来对应不同属性修改的回调函数。 **射线拾取**步骤: > 注:此处需要配合 `mousedown`和`mouseup` 判断鼠标行为是否为单击,拖动会对控制器造成影响。 - 绑定鼠标单击的回调函数,获取鼠标位置 - 将鼠标位置归一化处理,范围为$[-1, 1]$ - 创建射线,以相机为起点,连接鼠标位置为方向 - 获取与射线相交的按深度排序的物理列表 - 选出在当前对象列表中的三维对象 **动态更新与静态更新** 动态更新:借助`dat.gui`的`onChange()`函数来绑定正在动态更新的参数内容,以实现物理属性的动态的动画效果,但是该过程不记入日志中,不向数据库提交。 静态更新:借助`dat.gui`的`onFinishChange()`函数来绑定已更新结束的参数内容,且需要加工对应命令且提交,**注意**该提交**仅记入日志**,不需要重新解析。 说明: - 更新过程中,默认选取后物理材料颜色为茶绿色,若用户更新材料颜色,则取消选中后不恢复;否则,应恢复之前颜色。 - 由于`dat.gui`的双向绑定`listen()`函数,每次会生成一个新的`GUI`编辑器。 - 每次更新内容后,需要存储新的内容的值,以便在更新过程中根据值的改变判断是否需要重新渲染。 #### 动态自适应 **地面自适应** 三维画布上地面的大小会根据物理所占据的空间动态更新。 在前面提到创建物体时需要设置外层`cube-wrapper`便于用于此,遍历所有`cube-wrapper`取得所有物体所占据的`x,y,z`的范围,即最大值和最小值。根据该值,重新设置画布的大小。 **相机自适应** 关于视角转换组件以此为基础。 相机调整位置的过程中,会根据前面计算得出的范围来获取最大位置以调整远近。 - 全览视图的`x,y,z`分别乘以倍率`2.8,3,3`。 - 主视图,需调整至 z 轴 - 侧视图,需调整至 x 轴 - 俯视图,需调整至 y 轴 ### 部署说明 - 运行`npm run build`命令。 - 借助`Gitee Page`或服务器部署页面。 - 代理问题通过后端不限制跨域来解决。 ## 后端设计 ### 接口列表 #### 创建房间 **请求 URL:** ``` http://47.99.54.252:8000/createRoom/create ``` **请求方式:** ``` Post ``` **参数说明:** | 参数 | 必选 | 类型 | 说明 | | :------: | :--: | :----: | :----: | | userName | true | string | 用户名 | **返回示例:** ```javascript { "message": "创建房间成功", "status": 200, "data": { 008915 } } ``` #### 加入房间 **请求 URL:** ``` http://47.99.54.252:8000/searchRoom/search ``` **请求方式:** ``` Post ``` **参数说明:** | 参数 | 必选 | 类型 | 说明 | | :------: | :--: | :----: | :----: | | roomId | true | string | 房间号 | | userName | true | string | 用户名 | **返回示例:** ```javascript { "message": "房间内有同名用户,请更改您的用户名", "status": 403 } ``` #### WebSocket **URL**:[ws://47.99.54.252:8000/connect](ws://47.99.54.252:8000/connect) **支持格式**:JSON **请求方式**:ws **请求参数** | 参数 | 必选 | 类型 | 说明 | | --- | --- | ---- | ---- | | roomId | true | string | 房间号 | | userName | true | string | 用户名 | **通信响应结构** | 参数 | 必选 | 类型 | 说明 | | --- | --- | ---- | ---- | | message | true | string | 状态描述短语 | | status | true | int | 状态码 | | data | true | object | 通信数据 | **通信数据结构** - 历史命令 | 参数 | 必选 | 类型 | 说明 | 示例 | | --- | --- | ---- | --- | --- | | type | true | string | 数据结构类型说明 | "init" | | message | true | array | 历史命令 | [] | - 用户进入离开房间 | 参数 | 必选 | 类型 | 说明 | 示例 | | --- | --- | --- | --- | --- | | type | true | string | 数据结构类型说明 | room | | time | true | string | 消息产生时间 | "2022/02/24 22:00" | | user | true | string | 用户名 | "Frank" | | isJoin | true | boolean | 表示离开或进入 | true | - 命令 | 参数 | 必选 | 类型 | 说明 | 示例 | | --- | --- | --- | --- | --- | | type | true | string | 数据结构类型说明 | command | | time | true | string | 消息产生时间 | "2022/02/24 22:00" | | user | true | string | 用户名 | "Frank" | | content | true | string | 命令 | "add cube 1,1,1" | ### 数据库设计 - 使用Mysql数据库 #### 房间表 | 字段 | 类型 | 必填 | 说明 | 主键 | | --- | --- | ---- | --- | --- | | roomId | varchar(10) | true | 房间号 | true | #### 命令表 | 字段 | 类型 | 必填 | 说明 | 主键 | | --- | --- | ---- | --- | --- | | roomId | varchar(10) | true | 房间号 | false | | content | varchar(255) | true | 命令内容 | false | #### 用户表 | 字段 | 类型 | 必填 | 说明 | 主键 | | --- | --- | ---- | --- | --- | | roomId | varchar(10) | true | 房间号 | false | | username | varchar(255) | true | 用户名 | false | ### 功能说明 - 使用`koa-generator`创建项目 - 使用`koa-cors`中间件设置跨域 - 使用`koa-websocket`中间件进行websocket通信 ### 全局定义 - 全局房间实例集合:储存所有房间实例 - 全局事件分发器:监听和发布**销毁房间**事件 - 房间类:包括对房间内用户的操作 - 全局调度中心:将创建房间与加入房间进行**解耦** ### 创建房间通信 1. 接受到前端发送的请求,利用 **时间戳** 生成唯一房间号,并在 `room` 表中查询房间号是否重复 - 如果房间号重复,返回房间创建房间失败信息 2. 在全局调度中心中创建对应 **房间数组**,并将创建房间的用户放入数组。 3. 将房间号插入到 `room` 表中,并向前端返回创建房间成功信息和房间号 ### 加入房间通信 1. 路由守卫处理请求的参数,在全局**调度中心**中查找请求的房间号 - 如果房间号不存在,返回未找到此房间消息 2. 在 `roomUser` 表中查找用户名,判断加入房间的请求中的用户名是否与此房间中的其他用户同名 - 如果有同名用户,返回房间内有同名用户的信息 - 没有,将用户加入到对应房间数组中,返回加入房间成功的信息 ### WebSocket通信 #### 建立通信 1. 路由守卫。先对请求参数做过滤处理,在数据库中对请求参数中携带的房间号进行查询, * 若不存在此房间,返回连接失败消息 2. 判断该房间是否存在于**全局房间实例集合** * 若不存在,新建房间实例(设置房间的**最后发送时间**为当前时间的时间戳 ),加入**全局房间实例集合**,并给该房间安装**全局事件分发器** 3. 根据房间号从**全局房间实例集合**取出房间实例,该房间接受用户 #### 用户加入 1. 将用户存入该房间的**用户实例集合** 2. 在`roomUser`表中记录该用户 3. 对该房间内的用户广播**新用户加入**消息 4. 从`commands`表中取出命令,对取出的命令根据时间进行排序后发送给该用户 5. 给用户设置事件监听器 * 消息到来事件 1. 先对用户输入信息做校验 2. 若校验不通过,终止执行 3. 若校验通过,则在`commands`表中存储该命令 4. 在房间的**消息队列**队尾加入此命令,并在**消息队列**中对命令按**时间戳**排序 5. 下一次时间循环中,广播命令:取出**消息队列**队首命令,将其时间和**最后发送时间**比较 * 若队首命令的时间小于**最后发送时间**,说明队首命令存在延迟到达情况(存在一个命令,其时间大于此刻这个队首命令的时间,并且其已经被广播出去),则不广播此命令 * 否则说明此命令正常,开始广播,并设置**最后发送时间**为此命令的时间,从**消息队列**中删除此命令 * 断开连接事件 1. 执行**用户离开**操作 #### 用户离开 1. 从房间的**用户实例集合**中和**调度中心**删除用户实例 2. 从`roomUser`表中删除该用户记录 3. 对该房间内的用户广播**用户离开**消息 4. 检查该房间是否为空 * 房间不为空,不做处理 * 房间为空 1. 从`room`表中删除该房间记录 2. 从`commands`表中删除该房间相关命令记录 3. 触发**销毁房间**事件 4. **全局事件分发器**监听到**销毁房间**事件,在**全局房间实例集合**中删除该房间 ### 部署说明 - 安装 pm2 进程管理工具 - 将打包文件放入 `/public` - 运行 `pm2 start app.js` 启动服务器,监听端口8000