
三维橱柜——厨房户型生成开发方案
1.需求背景
需求:span>STORY #8470 厨房户型模型建立
设计稿:暂无
需求要点:
- 生成 初始户型预览图;
- 墙面后有按钮控制墙面是否 显示,有按钮控制是否 增加 新墙面,有按钮控制是否 删除 该墙面;
- 输入 墙面角度 和 墙面长度 后,预览页方可生成新墙面;
- 墙面数大于1时,最后一个墙面的终点与起始点之间会生成 自动闭合虚线;
- 墙面的角度和长度均合法时,有 墙面编号 一一对应;
- 墙面的角度合法,但长度为空时,显示按合法角度的闪烁的 引导箭头;
- 墙面角度或长度输入框获取焦点时,对应预览墙面变为 选中状态;
- 点击完成按钮后,最后的封闭线段由 虚线变为实线,表示该户型已经绘制完毕;
- 户型绘制完成后,生成三维墙面前,可重新点击墙面参数输入框直接 再次编辑;
- 户型绘制完成后,点击生成三维模型,可自动生成并显示 三维模型;
- 输入三边可求任意角度的组件。
2.实现分析
2.1 初始户型预览图及墙面参数控制
需求分析
生成初始户型预览图
a. 左侧参数区预设有3面墙面,且已有角度与长度值;
b. 右侧预览图根据左侧数据实时预览结果;
c. 点击眼睛按钮可以隐藏墙面,点击加号可在该墙面后新增墙面,点击减号可删除该墙面;
实现分析
页面组件实现
左侧墙面参数配置部分,可使用
Element-UI
表单组件;右侧预览效果可使用
Fabric.js
进行实时预览- 墙面可使用
Fabric.Line
实现; - 序号可以使用
Fabric.Text
实现; - 箭头可以使用
Fabric.Path
+Animation
实现;
- 墙面可使用
左侧参数区 数据结构:
xxxxxxxxxx
311let displayWallList = [{
2 index: 1,
3 angle: 90,
4 width: 200,
5 isShow: true,
6 isClosedLine: false
7}, {
8 index: 2,
9 angle: 90,
10 width: 200,
11 isShow: true,
12 isClosedLine: false
13}, {
14 index: 3,
15 angle: 90,
16 width: 200,
17 isShow: true,
18 isClosedLine: false
19}, {
20 index: -1,
21 angle: 90,
22 width: null,
23 isShow: false,
24 isClosedLine: false
25}, {
26 index: 4,
27 angle: 90,
28 width: 200,
29 isShow: true,
30 isClosedLine: true
31}]
- 生成右侧预览图所需数据的 数据结构:
xxxxxxxxxx
371let canvasWallList = [{
2 index: 1,
3 angle: 90,
4 width: 200,
5 start: { x: 0, y: 0 },
6 end: { x: 0, y: 200 },
7 isClosedLine: false,
8 isShowArrow: false,
9 isActive: false
10}, {
11 index: 2,
12 angle: 90,
13 width: 200,
14 start: { x: 0, y: 200 }, // 该点起点坐标为上一个点的终点坐标
15 end: { x: 200, y: 200 },
16 isClosedLine: false,
17 isShowArrow: false,
18 isActive: false
19}, {
20 index: 3,
21 angle: 90,
22 width: 200,
23 start: { x: 200, y: 200 }, // 该点起点坐标为上一个点的终点坐标
24 end: { x: 200, y: 0 },
25 isClosedLine: false,
26 isShowArrow: false,
27 isActive: false
28}, {
29 index: 4,
30 angle: 90,
31 width: 200,
32 start: { x: 200, y: 0 }, // 该点起点坐标为上一个点的终点坐标
33 end: { x: 0, y: 0 },
34 isClosedLine: true,
35 isShowArrow: false,
36 isActive: false
37}]
⭐️ 注意:基于Canvas的Fabric.js绘制时的坐标系(左)与用户主观认知的坐标系(右)存在区别,因此在绘制之前,可以统一使用用户主观认知的坐标系坐标(右),在准备绘制时,需要将坐标转换为Canvas的坐标(左)。
xxxxxxxxxx
81const originPosition = {x: 100, y: 800}
2
3function getCanvasPosition(position) {
4 return {
5 x: originPosition.x + position.x,
6 y: originPosition.y - position.y
7 }
8}
2.2 角度指引箭头
需求分析
在用户添加墙面后,光标聚焦在一组没有绘制完毕的数据时,可能会不清楚当前设置的角度是哪个角度。因此需要在预览区域添加一个角度指引箭头,用来告知用户当前设置的角度是哪个角度(如图中绿色箭头)。
实现分析
可以使用Fabric.js
添加图片或使用Fabric.Path
进行绘制。
- 默认箭头角度为 垂直于最后墙面(即90度);
- 默认箭头长度为 20px;
- 在预览数据(
canvasWallList
)中使用isShowArrow
控制是否显示箭头。
2.3 自动闭合虚线
需求分析
在墙面数量大于1时,期望最后一个墙面的顶点与初始点有一条自动闭合的虚线。
实现分析
可以 监控左侧墙面数据的数量,当其大于1时,自动连接最后一个墙面的结束点与初始起点,且用淡灰色虚线表示。
即当 canvasWallList
长度大于1时,将canvasWallList
最后一个点的终点与第一个点的起点连接起来。
xxxxxxxxxx
281function getClosedWall(){
2 let arr = canvasWallList
3 let firstWall = arr[0];
4 let lastWall = arr[arr.length - 1];
5
6 let getDistance = (start, end) => Math.sqrt(Math.pow(start.x - end.x, 2) + Math.pow(start.y - end.y, 2))
7 let width = getDistance(lastWall.end, firstWall.start)
8
9 let getAngle = (start, end) => Number((Math.acos(Math.abs(start.x - end.x) / width) / Math.PI * 180).toFixed(4))
10 let angle = getAngle(lastWall.end, firstWall.start)
11
12 canvasWallList.push({
13 index: lastWall.index + 1,
14 angle: angle,
15 width: width,
16 start: lastWall.end,
17 end: firstWall.start,
18 isClosedLine: true
19 })
20
21 displayWallList.push({
22 index: lastWall.index + 1,
23 angle: 0,
24 width: 0,
25 isShow: true,
26 isClosedLine: true
27 })
28}
2.4 墙面选中状态
需求分析
当用户编辑墙面参数时,为方便用户快速知道当前编辑的墙面是哪一个,右侧预览图,采用墙面高亮的形式进行提醒。
实现分析
右侧预览图在生成墙面时,将生成的墙面数据全部保存在数组里。当左侧参数输入框获取焦点时,可根据下标获取对应预览框中的墙面,先清空现有激活的墙面,随后将该墙面改为激活状态,再渲染预览图进行更新。
2.5 2D转3D
需求分析
根据用户绘制的平面房型图,待用户点击【3D化】按钮,输入厨房高度后,需要生成对应的三维模型图,且三维坐标原点应当在三维模型房间下地面中心。
实现分析
根据二维用户对墙面的操作,记录厨房户型墙面的数量、每个墙面的尺寸与角度,计算每个墙面在三维的坐标,尺寸与角度;
坐标:二维数据(
canvasWallList
)最终保存的为用户主观坐标系坐标,且已知三维房型高度(用户输入),坐标转换关系如下:- X: | 墙面起点的x坐标 – 墙面终点的x坐标 | / 2 – 户型最大宽度 / 2
- Y: 厨房高度 / 2
- Z: | 墙面起点的y坐标 – 墙面终点的y坐标 | / 2 – 户型最大深度 / 2
尺寸:
- height: 房间高度
- width: 墙面长度
旋转:
- X:0
- Y:- angle / 180 * Math.PI
- Z:0
- Demo:Plane Examples
将墙面结果转换成json配置文件;
xxxxxxxxxx
361const wallConfig = {
2"amount": 4,
3"data": [{
4"size": {
5"height": 1,
6"width": 2
7},
8"position": new BABYLON.Vector3(1, 0.5, 0),
9"rotation": new BABYLON.Vector3(0, Math.PI, 0),
10"name": 'front'
11}, {
12"size": {
13"height": 1,
14"width": 4
15},
16"position": new BABYLON.Vector3(0, 0.5, 2),
17"rotation": new BABYLON.Vector3(0, -Math.PI/2, 0),
18"name": 'left'
19}, {
20"size": {
21"height": 1,
22"width": 2
23},
24"position": new BABYLON.Vector3(1, 0.5, 4),
25"rotation": new BABYLON.Vector3(0, 0, 0),
26"name": 'behind'
27}, {
28"size": {
29"height": 1,
30"width": 4
31},
32"position": new BABYLON.Vector3(2, 0.5, 2),
33"rotation": new BABYLON.Vector3(0, Math.PI/2, 0),
34"name": 'right'
35}]
36}
Babylon.js中使用plane根据配置文件创建对应的墙面;
xxxxxxxxxx
151function createSingleWall(wallParams){
2console.log(wallParams)
3const f = new BABYLON.Vector4(0,0, 0.5, 1);
4const b = new BABYLON.Vector4(0.5,0, 1, 1);
5const mat = new BABYLON.StandardMaterial("");
6mat.diffuseTexture = new BABYLON.Texture("https://assets.babylonjs.com/environments/tile1.jpg");
7const plane = BABYLON.MeshBuilder.CreatePlane(wallParams.name, {frontUVs: f, backUVs: b, height: wallParams.size.height, width: wallParams.size.width});
8plane.material = mat
9plane.rotation = wallParams.rotation
10plane.position = wallParams.position
11}
1213for(let i =0;i<wallConfig.amount;i++){
14createSingleWall(wallConfig.data[i])
15}
Plane有
sideOrientation
属性可以用来控制墙面是否透明,Demo:Plane Examples
交互设计
- 当用户设置完2D户型的相关参数时,点击完成按钮,页面出现【3D化】按钮。待用户点击后,页面弹出加载框,页面根据 Json配置文件 进行渲染对应的三维模型。
3.方案设计
4.方案对比
4.1 已知起点与旋转角度,求终点坐标方案对比
描述:
a. 使用平面旋转
思路:
延长OA到B’点,使 AB’ = AB。
已知OA,AB,角a,可求A坐标,B’坐标;
根据A坐标与角b,可通过平面中,一个点绕任意点旋转a度后的点的坐标来求出B坐标
平面中,一个点(x,y)绕任意点(dx, dy)顺时针旋转a度后的坐标
- x= (x – dx)cos(-a) – (y – dy)sin(-a) + dx ;
- y= (x – dx)sin(-a) + (y – dy)cos(-a) +dy ;
平面中,一个点(x,y)绕任意点(dx,dy)逆时针旋转a度后的坐标
- x= (x – dx)cos(a) – (y – dy)sin(a) + dx ;
- y= (x – dx)sin(a) + (y – dy)cos(a) +dy ;
b. 使用向量求解
思路:
本质:OB = OA + AB(粗体代表向量)
先在A点建立平面直角坐标系。
OA向量用A点坐标(xA, yA)=> (OA * cos a, OA * sin a) 表示;OB向量可以用B点坐标(xB,yB)表示;
在A点的平面直角坐标系中,AB向量可以用B点坐标表示。
OB = OA + AB
且 AB 可以看作B点在以A点为坐标系时B的坐标,即(AB * cos c, AB * sin c)
c = a + b – 180
所以 OB = OA + AB =(OA * cos a, OA * sin a) +(AB * cos c, AB * sin c)=> (OA * cos a + AB * cos c, OA * sin a + AB * sin c) => (OAcos a + AB cos(a+b-180), OAsin a + AB sin(a+b-180))
即B点坐标:
- X: OAcos a + AB cos(a+b-180)
- Y: OAsin a + AB sin(a+b-180)
总结规律:
X: OA * cos (a + 0180) + AB cos (a+b – 1*180) + BC * cos (a+b+c – 2*180) + CD * cos (a+b+c +d – 3*180)….
Y: OA * sin (a + 0180) + AB sin (a+b – 1*180) + BC * sin (a+b+c – 2*180) + CD * sin (a+b+c +d – 3*180)….
方案对比
a方案:容易理解,所有的点坐标均可先依照第一组数据的角度进行旋转平移求出。但该方案为求出最终结果,中间求出太多无用的数据(以三个点为例);
- 为求 C 点坐标,需要利用 C’ 的坐标在 B 点旋转 d – 180°的角度。但 C’ 点坐标未知;
为求 C’ 点坐标,需要利用 C” 的坐标在 A 点旋转 180° – b 的角度,此时 C” 点坐标已知;
可知,为求 C 点坐标,该方案需要求出无用的 C’,C” 坐标。当墙面增多的时候,无用的坐标也会越多,因此舍弃。
b方案:
使用向量加法将墙面数据转换为向量,当计算新墙面数据时,需要累加之前的所有数据(例:OE = OA + AB + BC + CD + DE)。
但由于之前所有数据均为有效数据(即OA为A点坐标,AB可转换为B点坐标,并未产生无用数据)
xxxxxxxxxx
191let arr = [{angle: 90, width: 200}, {angle: 90, width: 200}, {angle: 90, width: 200}, {angle: 90, width: 200}];
2let xArr = [], yArr = [];
3let index = 0;
4let getRadian = (angle) => angle / 180 * Math.PI;
5while (index < arr.length) {
6let angle = 0;
7for (let i = 0; i <= index; i++) {
8angle += Number(arr[i].angle)
9}
10let x = arr[index].width * Math.cos(getRadian(angle - index * 180));
11let y = arr[index].width * Math.sin(getRadian(angle - index * 180));
12xArr.push(x);
13yArr.push(y);
14console.log(
15'X:', Number(eval(xArr.join("+")).toFixed(2)),
16'Y:', Number(eval(yArr.join("+")).toFixed(2)),
17'Real Angle:',angle - index * 180);
18index++
19}
4.2 三维生成墙面方案对比
a. 不规则多边形(Irregular Polygons
) + 不规则多边形拉伸(Irregular Polygon Extrusion
)
思路:
- 先使用不规则多边形(
Irregular Polygons
),通过各个点组成shape绘制出平面户型; - 再利用
ExtrudePolygon
拉伸成三维模型。
Demo:Irregular Polygon Extrusion Examples
b. 规则立方体
思路:
- 先用规则立方体Mesh按墙面尺寸生成墙面;
- 调整坐标,旋转角度拼接各个墙面;
- 利用布尔运算去除相邻墙面相交部分;
- 利用墙面法向量与照相机的夹角判断是否隐藏墙面(给mesh添加透明材质可以隐藏mesh)。
c. 使用平面
思路:
- 先获取墙面的尺寸,坐标,角度;
- 用平面生成各个墙面;
- 平面自带属性可自动隐藏。
Demo: Plane Examples
方案对比
- a方案: 尽管a方案由平面直接拉伸成3D十分方便,但a方案由于是一个整体,在墙面透明部分处理起来较麻烦,舍弃;
- b方案:墙面厚度可自调节;利用布尔运算与法向量可以处理墙面透明需求,但工作量较大;
- c方案:墙面厚度不可调节;平面自有属性可自动根据照相机情况进行调节墙面透明度情况。
目前初步 选用c方案 进行开发。
5.注意事项
- 左侧参数的编号:只有当角度值与长度值都合法的情况下才显示编号;
- 自动闭合墙面的数据不可修改,用灰色提醒,且只有是否显示按钮,没有增加与删除按钮(或不可操作);
- 光标聚焦在一组没有绘制完毕的数据时,显示箭头表明下一步绘制方向(默认90度),失焦后箭头隐藏;
- 左侧参数输入框获取焦点后,右侧预览图对应线段变为选中状态,失焦后恢复默认状态;