自定义编辑3D房间工具(二) 添加门等新模块、模块选中及删除

1,320 阅读6分钟

一期链接:自定义编辑3D房间工具(一) 画线成墙

很久没更新了,打工人,沉迷于ctrl + C 、 ctrl + V;最近才有空,又捡起了 这东西。。。。

基本效果:

一、原理回顾

canvas 画图 ------> 产生数据  ------> 渲染3D

二、添加了新模块

玻璃墙、门、自定义地板

canvas部分

内 的地板、墙等用简单的 线(ctx.lineTo)、 方块(ctx.fillRect)实现;详情,请看一期

这里特别要讲的是:画门

因为门,一定是在墙上的,且门的旋转角度一定和墙一致,所以画门的时候,不能像画墙一样。

它的交互应该是:点击墙上一点、则画出顺着墙方向的门,点击位置为门的中心位置。(点击位置需要根据墙的中心线位置修正,这样画出来的门才是墙里面居中的)

1、通过isPointInStroke 方法,判断 点击位置在哪个墙上,并使用该墙的参数

ctx.isPointInStroke(activePos[0], activePos[1])

2、修正门的坐标

// 获取门的坐标。点击位置,不一定是墙的中心线上的点,要简单处理下
const getDoorPos = (wallData: [[number, number], [number, number]], pos: [number, number]) => {
  let result = pos;
  const start = wallData[0];
  const end = wallData[1];
  if (end[0] - start[0] && end[1] - start[1]) {
    // 点击点不在墙的中线上
    // 墙函数
    const k = (end[1] - start[1]) / (end[0] - start[0]);
    const b = end[1] - k * end[0];
    // 点击点垂线b值
    const b2 = pos[1] + k * pos[0];
    // 垂线和墙函数的交点
    const y = (b + b2) / 2;
    const x = (b2 - b) / (2 * k);
    result = [x, y];
  }
  return result;
};

3、使用公共的画线的方法,画门

// 画门
        if (activeNow?.type === 'wall') {
          const params: any = {
            id: new Date().getTime(),
            type: active,
            door: getDoorPos(activeNow.data, endPo),
            wallId: activeNow.id,
            data: activeNow.data,
          };
          lines.push(params);
        }
const drawLine = (
  ctx: any,
  datas: [[number, number], [number, number]],
  config?: {
    type?: ActiveDrawType;
    width?: number;
  },
) => {
  if (!datas || !ctx || !datas.length) {
    return;
  }
  const { type = 'wall', width = 8 } = config || {};
  const wallColorNow = wallColor[type];
  const start = datas[0];
  const end = datas[1];
  ctx.beginPath(); //开始路径
  ctx.lineWidth = width;
  ctx.strokeStyle = wallColorNow;
  ctx.setLineDash([]);
  ctx.moveTo(start[0], start[1]); //定义路径起始点
  ctx.lineTo(end[0], end[1]); //路径的去向
  ctx.closePath();
  ctx.stroke();
};

three部分

1、注意:bsp和threejs版本

我使用的:three版本:"@types/three": "^0.126.1",

bsp: 我用的是网上大佬自己改的。(非正式版本)我截取了部分关键的bsp调整代码,放到文章最后。

2、umi 与 threejs/fiber

我业务架构使用 umi ,因为umi 会对 prop.children进行统一处理。导致我threejs/fiber用不了!!! (这是我的部分吐槽)

bsp挖孔代码、基础墙代码

//墙上挖门,通过两个几何体生成BSP对象
  function createResultBsp(bsp: any, less_bsp: any, mat: any) {
    let material = wallGrayMaterial;
    switch (mat) {
      case 1:
        material = wallPurpleMaterial;
        break;
      case 2:
        material = wallGrayMaterial;
        break;
    }
    const sphere1BSP = new ThreeBSP(bsp);
    const cube2BSP = new ThreeBSP(less_bsp);
    const resultBSP = sphere1BSP.subtract(cube2BSP);
    const result = resultBSP.toMesh(material);
    result.material.flatshading = THREE.FlatShading;
    // result.geometry.computeFaceNormals();  //重新计算几何体侧面法向量
    result.geometry.computeVertexNormals();
    result.material.needsUpdate = true; //更新纹理
    result.geometry.buffersNeedUpdate = true;
    result.geometry.uvsNeedUpdate = true;
    return result;
  }

//返回墙对象
  function returnWallObject(
    width: any,
    height: any,
    depth: any,
    angle: any,
    material: any,
    x: any,
    y: any,
    z: any,
  ) {
    const cubeGeometry = new THREE.BoxGeometry(width, height, depth);
    const cube = new THREE.Mesh(cubeGeometry, material);
    cube.position.x = x;
    cube.position.y = y;
    cube.position.z = z;
    cube.rotation.y = angle;
    return cube;
  }

// 有玻璃的镂空墙
  function createGlassWall(datas: any[]) {
    const { height = 0, glassHeight = 0, glassDepth = 1 } = configWall || {};
    const left_wall = createCubeWall(datas, {
      material: matArrayB,
    });
    const left_cube = createCubeWall(datas, {
      material: matArrayB,
      height: glassHeight,
      y: (height - glassHeight) / 2,
    });
    const wallBsp = createResultBsp(left_wall, left_cube, 1);
    const cube = createCubeWall(datas, {
      material: glass_material,
      height: glassHeight,
      y: (height - glassHeight) / 2,
      depth: glassDepth,
    });
    const bspGroup = new THREE.Group();
    bspGroup.add(wallBsp);
    bspGroup.add(cube);
    return bspGroup;
  }

三、模块选中及删除

核心思路:点击判断选中的模块,画选中线,点击按钮,批量删除

1、模块选中的判断

线的判断:ctx.isPointInStroke(activePos[0], activePos[1])

地板块的判断:containStroke(item.data, activePos[0], activePos[1])

const drawBoxs = ({
  ctx,
  lines,
  activeLines,
  activePos,
}: // endPo,
// active,
{
  ctx: any;
  lines: Record<string, any>[];
  activeLines?: string[];
  activePos?: any[];
  endPo?: any[];
  active?: string;
}) => {
  let activeItem: any;
  lines.forEach((item: any) => {
    let func: any = drawLine;
    const params: any = { type: item.type };
    if (item.type === 'floor') {
      func = drawFloor;
    } else if (item.type === 'door') {
      func = drawDoor;
      params.door = item.door;
    }
    func(ctx, item.data, params);
    // 判断当前选中线、或者地板
    if (activePos && activePos.length) {
      if (item.type === 'floor') {
        if (containStroke(item.data, activePos[0], activePos[1])) {
          activeItem = item;
        }
      } else if (ctx.isPointInStroke(activePos[0], activePos[1])) {
        activeItem = item;
      }
    }
    // 画选中的框
    if (activeLines?.includes(item.id)) {
      let start = item.data[0];
      let end = item.data[1];
      if (item.type === 'door') {
        [start, end] = getDoorData(start, end, item.door);
      }
      drawBorder({
        ctx,
        isRect: item.type === 'floor',
        start,
        end,
      });
    }
  });

  return activeItem;
};

2、绘制选中虚线

原理:

已知 start、end 两个点坐标。已知墙宽度。

计算 a、b、c、d四个点坐标。

ok,使用我们的三角函数啥啥啥的。(可怜我一个社会老狗子,三角函数居然忘了)

// 绘制选中 虚线框
const drawBorder = (props: {
  ctx: any;
  lineWidth?: number;
  strokeStyle?: string;
  lineDash?: [number, number];
  start: [number, number];
  end: [number, number];
  width?: number;
  isRect?: boolean;
  isDoor?: boolean;
}) => {
  const {
    ctx,
    lineWidth = 2,
    strokeStyle = '#f5222d',
    lineDash = [6, 6],
    width = 8,
    start,
    end,
    isRect,
  } = props || {};
  ctx.beginPath(); //开始路径
  ctx.lineWidth = lineWidth;
  ctx.strokeStyle = strokeStyle;
  ctx.setLineDash(lineDash);
  // 地板块的,虚线框简单
  if (isRect) {
    ctx.strokeRect(start[0], start[1], end[0] - start[0], end[1] - start[1]);
  } else {
    let point1x = 0,
      point1y = 0;
    const width2 = width / 2;
    if (end[0] - start[0]) {
      // 墙的斜率,我们要使用的墙切面的斜率 -k
      const k = (end[1] - start[1]) / (end[0] - start[0]); 
      const startb = start[1] + k * start[0];
      // +(0, b)点,计算cos\sin然后计算,新的点相对原来点位移的x,y
      const spoint0Y = start[1] - startb; 
      const spoint0StartXY = Math.pow(Math.pow(spoint0Y, 2) + Math.pow(start[0], 2), 0.5);
      point1y = width2 * (start[0] / spoint0StartXY);
      point1x = width2 * (spoint0Y / spoint0StartXY);
    } else {
      // 立着的时候 单独处理下
      point1x = width2;
    }
    ctx.moveTo(start[0] + point1x, start[1] + point1y); //定义路径起始点
    ctx.lineTo(start[0] - point1x, start[1] - point1y);
    ctx.lineTo(end[0] - point1x, end[1] - point1y);
    ctx.lineTo(end[0] + point1x, end[1] + point1y);
    ctx.lineTo(start[0] + point1x, start[1] + point1y);
    ctx.closePath();
  }
  ctx.stroke();
};

3、删除

删除,没啥好说的。我们一开始就将渲染和数据分离。

这里直接删除对应数据就好了。

四、结束

在座的各位大佬,新年快乐

BSP关键调整代码

ThreeBSP.prototype.toTree = function (treeIsh) {
    if (treeIsh instanceof ThreeBSP.Node) {
      return treeIsh;
    }
    // 看Three.js 0.126源码,各类Geometry基本都是继承或由BufferGeometry实现,基本上THREE.Geometry就废弃了,所以需要将获取点、法向量、uv信息要从以前的THREE.Geometry的faces中获取改为从THREE.BufferGeometry的attributes中获取
    var polygons = [],
      geometry =
        treeIsh === THREE.BufferGeometry || (treeIsh.type || '').endsWith('Geometry')
          ? treeIsh
          : treeIsh.constructor === THREE.Mesh
          ? (treeIsh.updateMatrix(), (this.matrix = treeIsh.matrix.clone()), treeIsh.geometry)
          : void 0;
    if (geometry && geometry.attributes) {
      // TODO 暂时就不对geometry.attributes中的position、 normal和uv进行非空验证了,日后有时间在说吧,正常创建的BufferGeometry这些值通常都是有的
      var attributes = geometry.attributes,
        normal = attributes.normal,
        position = attributes.position,
        uv = attributes.uv;
      // 点的数量
      var pointsLength = attributes.position.array.length / attributes.position.itemSize;

      // 如果索引三角形index不为空,则根据index获取面的顶点、法向量、uv信息
      if (geometry.index) {
        var pointsArr = [],
          normalsArr = [],
          uvsArr = [];
        // 从geometry的attributes读取点、法向量、uv数据
        for (var i = 0, len = pointsLength; i < len; i++) {
          // 通常一个点和一个法向量的数据量(itemSize)是3,一个uv的数据量(itemSize)是2
          var startIndex = 3 * i;
          pointsArr.push(
            new THREE.Vector3(
              position.array[startIndex],
              position.array[startIndex + 1],
              position.array[startIndex + 2],
            ),
          );
          normalsArr.push(
            new THREE.Vector3(
              normal.array[startIndex],
              normal.array[startIndex + 1],
              normal.array[startIndex + 2],
            ),
          );
          uvsArr.push(new THREE.Vector2(uv.array[2 * i], uv.array[2 * i + 1]));
        }
        var index = geometry.index.array;
        for (var i = 0, len = index.length; i < len; ) {
          var polygon = new ThreeBSP.Polygon();
          // 将所有面都按照三角面进行处理,即三个顶点组成一个面
          for (var j = 0; j < 3; j++) {
            var pointIndex = index[i],
              point = pointsArr[pointIndex];
            var vertex = new ThreeBSP.Vertex(
              point.x,
              point.y,
              point.z,
              normalsArr[pointIndex],
              uvsArr[pointIndex],
            );
            vertex.applyMatrix4(this.matrix);
            polygon.vertices.push(vertex);
            i++;
          }
          polygons.push(polygon.calculateProperties());
        }
      } else {
        // 如果索引三角形index为空,则假定每三个相邻位置(即相邻的三个点)表示一个三角形
        for (var i = 0, len = pointsLength; i < len; ) {
          var polygon = new ThreeBSP.Polygon();
          // 将所有面都按照三角面进行处理,即三个顶点组成一个面
          for (var j = 0; j < 3; j++) {
            var startIndex = 3 * i;
            var vertex = new ThreeBSP.Vertex(
              position.array[startIndex],
              position.array[startIndex + 1],
              position.array[startIndex + 2],
              new THREE.Vector3(
                normal.array[startIndex],
                normal.array[startIndex + 1],
                normal.array[startIndex + 2],
              ),
              new THREE.Vector2(uv.array[2 * i], uv.array[2 * i + 1]),
            );
            vertex.applyMatrix4(this.matrix);
            polygon.vertices.push(vertex);
            i++;
          }
          polygons.push(polygon.calculateProperties());
        }
      }
    } else {
      console.error('初始化ThreeBSP时为获取到几何数据信息,请检查初始化参数');
    }
    return new ThreeBSP.Node(polygons);
  };