上节在 json 里添加了墙的数据,并且把它绘制了出来:

这节我们来加上门窗。
先看下酷家乐的:
比如这样的门窗:

在 2d 平面会有对应的标识:

这个门窗是可以调整大小的:

可以看到,在 2d 平面了调整了窗户大小,3d 视图的窗户也小了。
而且窗户可以更换:

可以移动位置:

那如何在墙上画出这样的门窗呢?
首先,我们用 ExtrudeGeometry 拉伸出墙壁的时候,要留下窗户和门的洞,这个洞的大小位置也要记录下来。
然后在洞的位置加载一个门窗的模型,调整门窗大小的话就是设置模型的 scale。
我们来写一下:
之前我们是由底部的 4 个点向上拉伸出墙体:

但我们要挖洞的话,应该是从墙面向一侧水平拉伸。
所以我们改一下存储的结构:
比如这样一面墙:



我们只存储底部的两个端点的位置:
大概是这样的结构:

墙底部的左右端点位置,墙的高度、厚度。
窗户的洞的左下角位置,窗户宽高。
有了这些就知道怎么画了。
walls: [
{
left: {x: 0, z: 0},
right: {x: 500, z: 0},
height: 500,
depth: 30,
windows: [
{
leftBottomPosition: {
x: 100,
z: 100
},
width: 300,
height: 300
}
]
}
]
类型也要改一下:

interface Wall {
left: {
x: number,
z: number
},
right: {
x: number,
z: number
},
height: number,
depth: number,
windows: [
{
leftBottomPosition: {
x: number,
z: number
},
width: number,
height: number
}
]
}
把之前绘制 2d 视图的代码注释掉:

我们先绘制 3d 的:

const scene = scene3DRef.current!;
const walls = data.walls.map(item => {
const shape = new THREE.Shape();
shape.moveTo(item.left.x, item.left.z);
shape.lineTo(item.right.x, item.right.z);
shape.lineTo(item.right.x, item.right.z + item.height);
shape.lineTo(item.left.x, item.left.z + item.height);
shape.lineTo(item.left.x, item.left.z);
const geometry = new THREE.ExtrudeGeometry(shape, {
depth: item.depth
});
const material = new THREE.MeshPhongMaterial({
color: 'white'
})
const wall = new THREE.Mesh(geometry, material);
// wall.rotateX(-Math.PI/2);
return wall;
});
scene.add(...walls);
同样是从左下角位置、右下角位置、右上角位置、左上角位置,最后回到左下角位置。
按照这个顺序来画墙的形状,然后拉伸一定的厚度。
这样拉伸就不用旋转了。
看下效果:

这样墙就画出来了。
然后我们来挖孔:

item.windows.forEach(win => {
const path = new THREE.Path();
const { x, z } = win.leftBottomPosition;
path.moveTo(x, z);
path.lineTo(x + win.width, z);
path.lineTo(x + win.width, z + win.height);
path.lineTo(x, z + win.height);
path.lineTo(x, z);
shape.holes.push(path);
})
同样是按照从左下角到右下角、右上角、左上角来转一圈的顺序来画。
把它添加到墙的 holes 里。
看下效果:

这样洞就挖好了。
AxesHelper 加长一下:


改一下数据试试:




没啥问题。
案例代码上传了小册仓库
总结
这节我们实现了门窗的存储和绘制。
上节的墙体数据存储有些问题,我们改了一下:
存储墙的两个端点还有墙的高度、厚度信息,并且存储 holes 的信息,包括左下角坐标,洞的宽度、高度。
这样,门窗的洞就预留好了,下节我们在洞的位置放上门窗。