前端实现拆楼?别怕!拿捏住🤏🏻

功能解析

我们一般提到拆楼,其实就是单独的显示某一层的模型,为了实现这个功能,我们在数据层面就得先准备好。这里我们建议把每一层的模型都单独发布,让图层以楼层为结构,后续在实现我们的功能就会非常简单。

注意:我们需要提前在Explorer中导入这些分层模型数据。

我这里也给大家准备了一份测试数据,就是图片上的数据,另外也把本次文章的代码共享给大家,大家可以下载学习一下。

百度网盘:pan.baidu.com/s/1H3-nH50l...

实现拆楼一般有两种方式:显隐拆楼与位移拆楼,咱们一一来解决一下。

显隐拆楼

其实就是显示当前的楼层,然后把在这层之上的模型全部隐藏,比如我显示5楼,我就把5楼以上的模型全部隐藏,其实就实现了单独查看楼层的功能。

实现逻辑

实现逻辑很简单,这里先简单列举一下:

  1. 获取图层树信息,让图层名称与id绑定。(涉及对象 infotree)
  2. 点击楼层事件:
    1. 创建两个数组,分别存放隐藏图层id和显示图层id。(比当前楼层高就隐藏,低就显示)
    2. 执行api,让图层进行显隐藏。(涉及对象 tileLayer)
  3. 重置事件:把图层全部设置为显示

实现步骤

1.前置:图层名称与id绑定

我们知道,使用飞渡api的时候,传参都是用的id,但图层的id普遍都是随机生成的,没有实际的概念。所以我们一般会用图层名称去绑定id,一是让代码的可读性更高,二是方便后续维护。

这里我们先要创建一个数组,存储对应的图层名称,这里的图层名称要和Explorer工程的名称对应,并且要从低到高排列,方便后续判断。再创建一个对象,用于存储名称与id的对应关系,方便接口调用。

javascript 复制代码
const buildNameList = ['L1-4', 'L5', 'L6', 'L7', 'L8', 'L9', 'L10', 'L11', 'L12', 'L13']   // 拆楼建筑名称数组
const buildIdObj = {}    // 拆楼建筑对象,保存id

在场景初始化后,为了清除上个用户执行的操作,需要先用fdapi.reset()重置一下视频流。再通过fdapi.infoTree.get()获取图层树信息,获取的信息大概是长这样的,这里列举了拆楼相关的对象。

json 复制代码
{
"infotree":[
{"iD":"5D6728F44BEAA533268D6EA04ADC368F","index":119,"parentIndex":118,"name":"L1-4","visiblity":true,"type":"EPT_Scene"},{"iD":"70EEA6304541BFB27E6B618878888B40","index":120,"parentIndex":118,"name":"L5","visiblity":true,"type":"EPT_Scene"},{"iD":"261F27DE488D97B44A02269B1822B71D","index":121,"parentIndex":118,"name":"L6","visiblity":true,"type":"EPT_Scene"},{"iD":"6F5F764C4E3C1D5449744D9682BD5AD3","index":122,"parentIndex":118,"name":"L7","visiblity":true,"type":"EPT_Scene"},{"iD":"19290B504B590F6D0C10BC9338C56FA9","index":123,"parentIndex":118,"name":"L8","visiblity":true,"type":"EPT_Scene"},{"iD":"071393574557CA6CE69885A50902E490","index":124,"parentIndex":118,"name":"L9","visiblity":true,"type":"EPT_Scene"},{"iD":"69D4CB194720B94227E0AEABF37BB186","index":125,"parentIndex":118,"name":"L10","visiblity":true,"type":"EPT_Scene"},{"iD":"D3B74FE946F47778DD3D649093F8344E","index":126,"parentIndex":118,"name":"L11","visiblity":true,"type":"EPT_Scene"},{"iD":"FA364521477CAFC950D2B0A8BC68C1B3","index":127,"parentIndex":118,"name":"L12","visiblity":true,"type":"EPT_Scene"},{"iD":"BD8B0B704427DBAB94779FB6075C6D5F","index":128,"parentIndex":118,"name":"L13","visiblity":true,"type":"EPT_Scene"}]
}

所以这里我们获取回调后的infotree对象,做一个循环,把图层名称与id放入到一个对象中,做对应关系,方便后续调用。下面就是初始化后执行的代码。

javascript 复制代码
        const __DigitalTwinPlayer = new DigitalTwinPlayer(HostConfig.Player, {
          domId: 'player',
          apiOptions: {
            //事件监听回调函数
            onReady: async () => {
              // 重置视频流
              await fdapi.reset(2 | 4)

              // 获取图层树信息,让图层名称与id绑定
              const { infotree } = await fdapi.infoTree.get()
              infotree.forEach(item => {
                if (buildNameList.includes(item.name)) {
                  buildIdObj[item.name] = item.iD
                }
              })
            }
          }
        })

2.楼层点击事件

首先创建两个数组,分别存放隐藏图层id和显示图层id。

点击楼层后,把当前楼层的名称获取到。然后循环图层名称数组,默认都push到显示id数组中,当循环名称与点击名称一致时,后面的id就push到隐藏数组中。

最后分别通过__g.tileLayer.show()__g.tileLayer.hide(),去显示隐藏对应的图层即可。

javascript 复制代码
      // 选择楼层
      const chooseFloor = name => {
        const showList = [],
          hideList = []

        // 高于选择楼层就要隐藏,这边做个判断
        let isHide = false
        buildNameList.forEach(itemName => {
          isHide ? hideList.push(buildIdObj[itemName]) : showList.push(buildIdObj[itemName])
          if (itemName == name) isHide = true
        })

        // 操作模型
        __g.tileLayer.show(showList)
        __g.tileLayer.hide(hideList)
      }

3.重置模型事件

这个方法比较简单,就是获取所有图层的id,然后都显示出来,这里就不再赘述。

javascript 复制代码
      // 重置模型
      const reset = () => {
        const idList = []
        for (let key in buildIdObj) idList.push(buildIdObj[key])
        __g.tileLayer.show(idList)
      }

把这些实现完后,咱们的显隐拆楼就完成了。

实现代码

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>页面初始化</title>
    <link rel="stylesheet" href="./lib/styles.css" />
	
      <!-- 按需替换dts ac.min.js路径 -->
    <script type="text/javascript" src="./lib/aircity/ac_conf.js"></script>
    <script type="text/javascript" src="./lib/aircity/ac.min.js"></script>
  </head>
  <body>
    <div id="player"></div>

    <div class="btn" onclick="reset()">重置模型</div>

    <div class="build-list">
      <div class="build-item" onclick="chooseFloor('L13')">L13</div>
      <div class="build-item" onclick="chooseFloor('L12')">L12</div>
      <div class="build-item" onclick="chooseFloor('L11')">L11</div>
      <div class="build-item" onclick="chooseFloor('L10')">L10</div>
      <div class="build-item" onclick="chooseFloor('L9')">L9</div>
      <div class="build-item" onclick="chooseFloor('L8')">L8</div>
      <div class="build-item" onclick="chooseFloor('L7')">L7</div>
      <div class="build-item" onclick="chooseFloor('L6')">L6</div>
      <div class="build-item" onclick="chooseFloor('L5')">L5</div>
      <div class="build-item" onclick="chooseFloor('L1-4')">L1-4</div>
    </div>

    <script type="text/javascript">
      // 拆楼建筑名称数组
      const buildNameList = ['L1-4', 'L5', 'L6', 'L7', 'L8', 'L9', 'L10', 'L11', 'L12', 'L13']
      // 拆楼建筑对象,保存id
      const buildIdObj = {}

      /**
       * 初始化场景
       */
      const OnLoad = () => {
        const __DigitalTwinPlayer = new DigitalTwinPlayer(HostConfig.Player, {
          domId: 'player',
          apiOptions: {
            //事件监听回调函数
            onReady: async () => {
              // 重置视频流
              await fdapi.reset(2 | 4)

              // 获取图层树信息,让图层名称与id绑定
              const { infotree } = await fdapi.infoTree.get()
              infotree.forEach(item => {
                if (buildNameList.includes(item.name)) {
                  buildIdObj[item.name] = item.iD
                }
              })
            }
          }
        })
      }

      // 选择楼层
      const chooseFloor = name => {
        const showList = [],
          hideList = []

        // 高于选择楼层就要隐藏,这边做个判断
        let isHide = false
        buildNameList.forEach(itemName => {
          isHide ? hideList.push(buildIdObj[itemName]) : showList.push(buildIdObj[itemName])
          if (itemName == name) isHide = true
        })

        // 操作模型
        __g.tileLayer.show(showList)
        __g.tileLayer.hide(hideList)
      }

      // 重置模型
      const reset = async () => {
        const idList = []
        for (let key in buildIdObj) idList.push(buildIdObj[key])
        __g.tileLayer.show(idList)
      }

      /**
       * 自适应分辨率
       */
      const onResize = async () => {
        let playerView = document.getElementById('player')
        playerView.style.height = window.innerHeight + 'px'
        playerView.style.width = window.innerWidth + 'px'
      }

      // 页面加载调用
      window.addEventListener('load', OnLoad, true)

      // 页面窗口变换调用
      window.addEventListener('resize', onResize, true)
    </script>
  </body>
</html>

位移拆楼

实现位移拆楼有两个阶段

  • 模型开启:把模型设置成开启状态,先把所有楼层都往上位移,并且都保留间距。

  • 抽拉楼层:点击楼层后把对应的楼层单独位移抽拉出来。

实现步骤

位移的实现逻辑会比显隐的方式复杂一些,这里也先简单列举一下:

  1. 获取图层树信息,绑定图层对应信息。(涉及对象 infotree)
    1. 让图层名称与id绑定。
    2. 记录模型的初始位置,方便后续重置模型。
    3. 记录模型的开启位置,计算楼层开启后每个图层对应的z值做保存,方便后续开启模型。
  2. 楼层位移事件:封装一个方法,让模型可以从起点位移到终点,并且位移速度需要可控。
  3. 模型开启:把每个模型都往上做位移,从初始位置位移到开启位置。
  4. 点击楼层事件:
    1. 计算抽拉位置:计算模型位移后的点位,通过方向、距离两个参数计算。
    2. 如果当前模型没有其他楼层展开,则直接把点击图层位移到抽拉位置。
    3. 如果当前模型有其他楼层展开,则还需要把之前的图层收回。(收回就是回到开启位置)
    4. 如果点击的和当前展开的是同一层,则直接收回当前图层。
  5. 重置事件:把图层全部设置为初始位置

从以上步骤我们可以分析出需要的全局参数

  • 状态参数
    • 建筑开启状态-Boolean:用于记录建筑是否开启,如果没开启就不能点击楼层和重置模型。
    • 当前建筑展开的楼层-String:在点击楼层事件中做判断使用。
    • 模型当前是否在移动-Boolean:如果模型还在移动,就禁止其他事件,防止交互阻塞。
  • 效果参数
    • 建筑抬升高度-Number:用于计算模型的开启位置。
    • 动画帧数-Number:位移的动画需要多少帧完成,dts都是异步操作,直接用速度会把握不准,所以使用帧来代替速度。
    • 楼层位移距离-Number:用于计算楼层的抽拉位置。
    • 楼层位移角度-Number:用于计算楼层的抽拉位置。

具体方法

1.前置:定义全局参数,绑定图层信息

和前面一致,需要定义一个数组存储图层名称。再创建一个对象,用于存储每个图层的必要信息。再定义上述提到的全局参数。

javascript 复制代码
const buildNameList = ['L1-4', 'L5', 'L6', 'L7', 'L8', 'L9', 'L10', 'L11', 'L12', 'L13']
const buildInfo = {}

// 状态参数
let openState = false, // 建筑开启状态
	openFloor = '', // 建筑开启的楼层
	moveState = false // 是否正在移动(动画)

// 效果参数
const fps = 8, // 动画需要多少帧完成
	risingHeight = 5, // 建筑抬升高度
	distance = 20, // 拆楼距离
	angle = 270 // 拆楼角度,0是x轴即正东方向,逆时针计算

执行场景初始化,并在初始化完成后,获取图层树信息。保存三个信息:id、初始位置、开启位置。

id可以直接获取,初始位置则需要通过fdapi.tileLayer.get()获取图层详细信息,我们拿一个图层举例,获取的信息大致如下:

json 复制代码
{
    "data":[
    {
        "id": "5D6728F44BEAA533268D6EA04ADC368F",
        "groupId": "",
        "userData": "",
        "dbTabId": "AB1EE06C468C21FA1C46578838AEDB6F",
        "fileName": "H:/work/客户成功-培训中心/专题视频/01二次开发实现拆楼/3dt/L1-4.3dt",
        "supportAttach": "true",
        "minVisibleHeight": -10000000,
        "maxVisibleHeight": 10000000,
        "bFlattenSupported": 0,
        "bbox": [493286.53,2492013.53, -3.521393,493336.769375,2491968.09,15.45922],
        "rotation": [ 0,0,0],
        "scale": [1, 1,1],
        "location": [493309.21,2491988.39,2.3]
    }
]
}

可以看到location就是初始位置的坐标,但开启位置需要通过全局参数risingHeight稍微计算一下,从下往上,每个模型的位移值都要再加上risingHeight的高度值,最终实现的代码如下:

javascript 复制代码
      const __DigitalTwinPlayer = new DigitalTwinPlayer(HostConfig.Player, {
        domId: 'player',
        apiOptions: {
          //事件监听回调函数
          onReady: async () => {
            fdapi.reset(2 | 4)

            // 获取图层树信息
            const { infotree } = await fdapi.infoTree.get()
            // 把图层id存放到一个数组中,方便查询属性
            const idList = []
            infotree.forEach(item => {
              if (buildNameList.includes(item.name)) {
                idList.push(item.iD)
              }
            })
            // 查询图层属性
            const { data } = await fdapi.tileLayer.get(idList)
            // 存储每个图层对应的属性
            data.forEach((item, index) => {
              buildInfo[buildNameList[index]] = {
                id: item.id,
                location: item.location,
                openLocation: [item.location[0], item.location[1], item.location[2] + (index + 1) * risingHeight]
              }
            })
          }
        }
      })

2.楼层位移事件

这里我们需要封装一个通用方法,传参是图层id和终点位置,我们能直接把对应id的模型,从当前的位置位移到终点位置,这里我们用到的方法就是fdapi.tileLayer.setLocation(),通过这个方法我们可以进行图层位移,但只能瞬间移动,不能做到平滑移动。

这里我们可以引用一个方法,通过两个点的坐标以及分段数,计算两个点之间的的点位坐标,如下所示:

javascript 复制代码
// 传参示例
getLineSegmentPoint([[0,0,20],[100,0,20]],20);

// 线段分点
function getLineSegmentPoint(lineSegment, interval) {
	try {
		if (interval && lineSegment && lineSegment.length === 2) {
			const point1 = lineSegment[0]
            const point2 = lineSegment[1]

            const a = point2[1] - point1[1]
            const b = point2[0] - point1[0]
            const c = point2[2] - point1[2]

            const o = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2) + Math.pow(c, 2))

            const n = o / interval
            const p = []

            for (let i = 1; i <= interval; i++) {
              const x = (b / o) * (n * i) + point1[0]
              const y = (a / o) * (n * i) + point1[1]
              const z = (c / o) * (n * i) + point1[2]
              p.push([x, y, z])
            }
            return p
		} else {
            console.error('线段取点失败', lineSegment, interval)
		}
	} catch (error) {
          console.error('线段取点失败', error)
	}
}

有了这个方法我们就在两个点直接有了非常多的点位,可以通过fdapi.tileLayer.setLocation()多次移动,做到平滑位移了。我们平常用其他系统大多数都是同步逻辑,在dts中api操作都是异步的,所以这里移动不需要写定时器,直接循环调用就可以做到逐帧调用代码了,具体代码如下:

javascript 复制代码
const fps = 20 // 全局参数
// 传参示例
setLocation({id:'123456',location:[100,0,20]});

// 模型位移,从模型当前位置位移到指定位置
const setLocation = param => {
 	// 设置移动状态为true
	moveState = true

	// 获取模型当前位置-通过tileLayer.get 获取模型信息
	const { data } = await fdapi.tileLayer.get(param.id)
    
	const startLocation = data[0].location
	const endLocation = param.location
    const pathPoints = getLineSegmentPoint([startLocation, endLocation], fps)
        
	// 每一帧根据路径点移动一次模型
	for (let f = 0; f <= fps - 1; f++) {
		fdapi.tileLayer.setLocation(param.id, pathPoints[f])
		// 最后一帧,把移动状态设置为false
		if (f == fps - 1) moveState = false
	}
}

单个模型的位移完成了,但我们有些情况不是单个图层位移,例如设置开启状态的时候,我们需要同时移动多个图层。这时候我们需要改造一下我们的方法,让他的输入参改为数组,每一帧循环调用fdapi.tileLayer.setLocation()方法去设置位移。但由于DTS的api是异步操作,所以会导致移动的时候会一步步执行,很显然不是我们想要的效果。 我们想要的效果是让这几个图层,在上升的时候,是同步进行上升的。这时候我们可以用到fdapi.tileLayerupdateBegin()updateEnd()方法。

在开始修改之前调用updateBegin(),然后可以多次调用setXXX()方法,最后调用updateEnd()提交修改更新数据。这样可以让两个方法中间的位移操作变成同步。改造后的代码如下:

javascript 复制代码
      // 模型位移,从模型当前位置位移到指定位置
      const setLocation = async arr => {
        // 设置移动状态为true
        moveState = true

        // 获取模型当前位置-通过tileLayer.get 获取模型信息
        const idList = []
        for (let index in arr) idList.push(arr[index].id)
        const { data } = await fdapi.tileLayer.get(idList)

        // 获取根据起点和终点,计算路径点
        for (let index in arr) {
          const startLocation = data[index].location
          const endLocation = arr[index].location
          arr[index].pathPoints = getLineSegmentPoint([startLocation, endLocation], fps)
        }

        // 每一帧根据路径点移动一次模型
        for (let f = 0; f <= fps - 1; f++) {
          fdapi.tileLayer.updateBegin()
          for (let index in arr) fdapi.tileLayer.setLocation(arr[index].id, arr[index].pathPoints[f], null)
          await fdapi.tileLayer.updateEnd()

          if (f == fps - 1) moveState = false
        }
      }

3.模型开启事件

模型开启就是把所有楼层从初始位置设置到开始位置,两个位置坐标在初始化的时候就已经存储好了。我们通过循环的方式直接批量的调用前面的楼层位移事件即可。

javascript 复制代码
      // 开始拆楼
      const begin = () => {
        if (openState) {
          console.log('模型已经开启,可点击楼层进行拆楼')
          return  
        }
        // 构造setLocationList id,对应结束的位置
        const setLocationList = []
        for (let key in buildInfo) {
          setLocationList.push({
            id: buildInfo[key].id,
            location: buildInfo[key].openLocation
          })
        }
        setLocation(setLocationList)

        // 打开状态设置为开启
        openState = true
      }

4.点击楼层事件

在实现楼层位移之前,我们需要通过全局参数的方向和距离来计算位移的终点位置,这里也是用了一个方法来计算,具体如下:

javascript 复制代码
//根据角度和距离计算点 传参:点位坐标[0,0,0],角度,距离
function calculatePoint(point, angle, distance) {
	const radians = angle * (Math.PI / 180) // 将角度转换为弧度
	const x1 = point[0] + distance * Math.cos(radians)
	const y1 = point[1] + distance * Math.sin(radians)
	return point[2] ? [x1, y1, point[2]] : [x1, y1]
}

有了这个方法后,我们只需要通过计算终点点位,再执行楼层位移事件即可,但我们还得分析一下一些特殊情况:

当楼层是开启状态时,点击楼层就会把对应的楼层抽拉出来,这时有下列几种情况

  1. 如果拆楼状态没开启,则直接返回。
  2. 如果模型仍在位移,则直接返回。
  3. 如果点击的是顶层,则直接返回(顶层已经可见,不需要抽拉)。
  4. 如果当前模型没有其他楼层展开,则直接把点击图层位移到抽拉位置。
  5. 如果当前模型有其他楼层展开,则还需要把之前的图层收回。(回到开启位置即可)
  6. 如果点击的和当前展开的是同一层,则直接收回当前图层。

所以我们需要判断不同的条件,执行一些不同的操作,最终的实现如下:

javascript 复制代码
   // 点击某一层
      const chooseFloor = name => {
        if (!openState) {
          console.log('请先开启拆楼')
          return
        }
        if (moveState) {
          console.log(='模型位移中,请等模型位移结束再操作')
          return
        }
        if (name === buildNameList[buildNameList.length - 1]) {
          console.log( '顶层不拆')
          return
        }

        const setLocationList = []

        // 如果已经有打开的楼层,则要把当前楼层回收
        if (openFloor) {
          setLocationList.push({ id: [buildInfo[openFloor].id], location: buildInfo[openFloor].openLocation })

          // 同一层则直接重置当前楼层
          if (openFloor == name) {
            setLocation(setLocationList)
            openFloor = ''
            return
          }
        }

        // 需要位移的楼层:通过起点、角度、距离 计算终点位置
        const startLocation = JSON.parse(JSON.stringify(buildInfo[name].openLocation))
        const endLocation = calculatePoint(startLocation, angle, distance)
        setLocationList.push({ id: [buildInfo[name].id], location: endLocation })
        setLocation(setLocationList)
        openFloor = name // 保存当前楼层信息
      }

5.重置模型事件

这个方法比较简单,就是获取所有图层的id,然后都设置成初始位置,再把状态参数都设置为默认,这里就不再赘述。

javascript 复制代码
// 重置模型
const reset = () => {
	if (!openState) {
		console.log('模型不需要重置')
		return
	}
	fdapi.tileLayer.updateBegin()
	for (let key in buildInfo) {
		fdapi.tileLayer.setLocation(buildInfo[key].id, buildInfo[key].location)
	}
	fdapi.tileLayer.updateEnd()
	openState = false
	openFloor = ''
}

把这些实现完后,咱们的位移拆楼就完成了。

实现代码

javascript 复制代码
      const buildNameList = ['L1-4', 'L5', 'L6', 'L7', 'L8', 'L9', 'L10', 'L11', 'L12', 'L13']
      const buildInfo = {}

      // 状态参数
      let openState = false, // 建筑开启状态
        openFloor = '', // 建筑开启的楼层
        moveState = false // 是否正在移动(动画)

      // 效果参数
      const fps = 20, // 动画需要多少帧完成
        risingHeight = 5, // 建筑抬升高度
        distance = 20, // 拆楼距离
        angle = 270 // 拆楼角度,0是x轴即正东方向,逆时针计算
      /**
       * 初始化场景
       */
      const OnLoad = () => {
        const __DigitalTwinPlayer = new DigitalTwinPlayer(HostConfig.Player, {
          domId: 'player',
          apiOptions: {
            //事件监听回调函数
            onReady: async () => {
              fdapi.reset(2 | 4)

              // 获取图层树信息
              const { infotree } = await fdapi.infoTree.get()
              // 把图层id存放到一个数组中,方便查询属性
              const idList = []
              infotree.forEach(item => {
                if (buildNameList.includes(item.name)) {
                  idList.push(item.iD)
                }
              })
              // 查询图层属性
              const { data } = await fdapi.tileLayer.get(idList)
              console.log(data)

              // 存储每个图层对应的属性
              data.forEach((item, index) => {
                buildInfo[buildNameList[index]] = {
                  id: item.id,
                  location: item.location,
                  openLocation: [item.location[0], item.location[1], item.location[2] + (index + 1) * risingHeight]
                }
              })
            }
          }
        })
      }

      // 开始拆楼
      const begin = () => {
        if (openState) {
          console.log('模型已经开启,可点击楼层进行拆楼')
          return
        }
        // 构造setLocationList id,对应结束的位置
        const setLocationList = []
        for (let key in buildInfo) {
          setLocationList.push({
            id: buildInfo[key].id,
            location: buildInfo[key].openLocation
          })
        }
        setLocation(setLocationList)

        // 打开状态设置为开启
        openState = true
      }

      // 点击某一层
      const chooseFloor = name => {
        if (!openState) {
          console.log('\x1b[33m%s\x1b[0m', '请先开启拆楼')
          return
        }
        if (moveState) {
          console.log('\x1b[33m%s\x1b[0m', '模型位移中,请等模型位移结束再操作')
          return
        }
        if (name === buildNameList[buildNameList.length - 1]) {
          console.log('\x1b[33m%s\x1b[0m', '顶层不拆')
          return
        }

        const setLocationList = []

        // 如果已经有打开的楼层,则要把当前楼层回收
        if (openFloor) {
          setLocationList.push({ id: [buildInfo[openFloor].id], location: buildInfo[openFloor].openLocation })

          // 同一层则直接重置当前楼层
          if (openFloor == name) {
            setLocation(setLocationList)
            openFloor = ''
            return
          }
        }

        // 需要位移的楼层:通过起点、角度、距离 计算终点位置
        const startLocation = JSON.parse(JSON.stringify(buildInfo[name].openLocation))
        const endLocation = calculatePoint(startLocation, angle, distance)
        setLocationList.push({ id: [buildInfo[name].id], location: endLocation })
        setLocation(setLocationList)
        openFloor = name // 保存当前楼层信息
      }

      // 模型位移,从模型当前位置位移到指定位置
      const setLocation = async arr => {
        // 设置移动状态为true
        moveState = true

        // 获取模型当前位置-通过tileLayer.get 获取模型信息
        const idList = []
        for (let index in arr) idList.push(arr[index].id)
        const { data } = await fdapi.tileLayer.get(idList)

        // 获取根据起点和终点,计算路径点
        for (let index in arr) {
          const startLocation = data[index].location
          const endLocation = arr[index].location
          arr[index].pathPoints = getLineSegmentPoint([startLocation, endLocation], fps)
        }

        // 每一帧根据路径点移动一次模型
        for (let f = 0; f <= fps - 1; f++) {
          fdapi.tileLayer.updateBegin()
          for (let index in arr) fdapi.tileLayer.setLocation(arr[index].id, arr[index].pathPoints[f], null)
          await fdapi.tileLayer.updateEnd()

          if (f == fps - 1) moveState = false
        }
      }

      // 重置模型
      const reset = () => {
        if (!openState) {
          console.log('\x1b[33m%s\x1b[0m', '模型不需要重置')
          return
        }

        fdapi.tileLayer.updateBegin()
        for (let key in buildInfo) {
          fdapi.tileLayer.setLocation(buildInfo[key].id, buildInfo[key].location)
        }
        fdapi.tileLayer.updateEnd()

        openState = false
        openFloor = ''
      }

      /**
       * 根据角度和距离计算点
       */
      function calculatePoint(point, angle, distance) {
        const radians = angle * (Math.PI / 180) // 将角度转换为弧度
        const x1 = point[0] + distance * Math.cos(radians)
        const y1 = point[1] + distance * Math.sin(radians)
        return point[2] ? [x1, y1, point[2]] : [x1, y1]
      }

      // 线段分点
      function getLineSegmentPoint(lineSegment, interval) {
        try {
          if (interval && lineSegment && lineSegment.length === 2) {
            const point1 = lineSegment[0]
            const point2 = lineSegment[1]

            const a = point2[1] - point1[1]
            const b = point2[0] - point1[0]
            const c = point2[2] - point1[2]

            const o = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2) + Math.pow(c, 2))

            const n = o / interval
            const p = []

            for (let i = 1; i <= interval; i++) {
              const x = (b / o) * (n * i) + point1[0]
              const y = (a / o) * (n * i) + point1[1]
              const z = (c / o) * (n * i) + point1[2]
              p.push([x, y, z])
            }
            return p
          } else {
            console.error('线段取点失败', lineSegment, interval)
          }
        } catch (error) {
          console.error('线段取点失败', error)
        }
      }

进阶:tick位移拆楼

使用上面的的位移方法,在移动帧设置的较大时,会有明显的卡顿现象,这是因为与视频流的交互是异步的。为了解决这种动画调用的问题,DTS有一个tick方法,可以把需要更新的代码传入到实例中,通过同步的方式直接运行,这样就可以避免掉帧卡顿。

tick介绍

用户经常需要使用定时器(setInterval)或者循环来执行一些非常频繁的接口调用,以实现一些自定义的动画效果,但是这些接口调用是通过JS API实现的, 因为Javascript是单线程执行的,太频繁的调用会影响浏览器的响应,另外Cloud的鼠标、键盘交互事件也是通过JS发送到后台的,频繁的接口调用会影响交互事件的传输,造成交互卡顿。 而且如果传输的数据量比较大时,也会占用不少带宽。

为了解决这个问题,实现更高效的API调用,tick实现了在C++每帧渲染的时候执行JS脚本以实现用户自定义的动画效果,这种实现比前端API调用有以下优点:

  • 高效,直接本机通过C++调用JS
  • 不影响鼠标、键盘交互,以及其他接口的调用
  • 不占用网络带宽

tick的使用

tick的具体使用方法和流程这里不再赘述,想知道更多tick相关可以查询官方的开发文档。

DTS API Tutorial: API_FrameTick (freedo3d.com)

我们实现拆楼,其实只需要3步

  1. 准备一个tick页面
  2. 在Cloud文件资源进行添加tick页面
  3. 客户端代码里执行fdapi.registerTick()fdapi.removeTck(),分别用于执行tick与移除tick。

具体方法

1.tick页面编写

在tick页面中,我们要把前面的 楼层位移事件 方法实现出来,接受客户端传来的id、终点位置、帧数,完成位移。

我们需要在tick页面初始化后,获取到起点位置,并通过 线段分点 的方法来根据帧数获取间隔点位。最后在tick()方法内逐帧位移,也就是执行fdapi.tileLayer.setLocation()改变模型位置。

在执行完最后帧后,要使用fdapi.removeTick()销毁tick页面,并通过FdExternal.postEvent('tick end')回调一个结束事件给到客户端,让客户端可以修改模型的移动状态,从而控制整个程序正常运行。

ps:FdExternal类封装了进程内JS与C++的互操作功能,详情可以看用户手册

html 复制代码
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>TICK</title>
    <!-- 按需替换路径 -->
    <script type="text/javascript" src="http://127.0.0.1:8090/libac"></script>
    <style type="text/css"></style>
  </head>

  <body></body>
</html>

<script>
  let i = 0
  let urlParam = window.location.search.slice(1)
  let urlParamArr = urlParam.split('&')

  let dataStr = urlParamArr.find(item => item.includes('data'))
  const arr = JSON.parse(decodeURI(dataStr.split('=')[1]))

  let fpsUrlArr = urlParamArr.find(item => item.includes('fps'))
  const fps = fpsUrlArr ? JSON.parse(decodeURI(fpsUrlArr.split('=')[1])) : 20

  const StartEndObj = {}

  let state = true

  window.onload = async function () {
    if (!arr) return

    new DigitalTwinAPI()

    const idList = []
    for (let index in arr) idList.push(arr[index].id)
    const { data } = await fdapi.tileLayer.get(idList)
    const tickArr = []

    for (let index in arr) {
      const startLocation = data[index].location
      const endLocation = arr[index].location

      const line = getLineSegmentPoint([startLocation, endLocation], fps)
      StartEndObj[data[index].id] = line
    }
  }

  function clientCalled(str) {
    document.getElementById('r3').innerText = str
  }

  function tick(frame) {
    if (!arr) return
    if (!state) return

    fdapi.tileLayer.updateBegin()
    for (let key in StartEndObj) fdapi.tileLayer.setLocation(key, StartEndObj[key][i], null)
    fdapi.tileLayer.updateEnd()

    if (i >= fps - 1) {
      ue.internal.postevent('tick end')
      i = 0
      state = false
      fdapi.removeTick()
    }
    i++
  }

  // 线段分点
  function getLineSegmentPoint(lineSegment, interval) {
    try {
      if (interval && lineSegment && lineSegment.length === 2) {
        const point1 = lineSegment[0]
        const point2 = lineSegment[1]

        const a = point2[1] - point1[1]
        const b = point2[0] - point1[0]
        const c = point2[2] - point1[2]

        const o = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2) + Math.pow(c, 2))

        const n = o / interval
        const p = []

        for (let i = 1; i <= interval; i++) {
          const x = (b / o) * (n * i) + point1[0]
          const y = (a / o) * (n * i) + point1[1]
          const z = (c / o) * (n * i) + point1[2]
          p.push([x, y, z])
        }
        return p
      } else {
        console.error('线段取点失败', lineSegment, interval)
      }
    } catch (error) {
      console.error('线段取点失败', error)
    }
  }

  function tick_next(o, frame) {}
</script>

2.Cloud文件资源添加

这一步比较简单,打开Cloud文件资源模块,打开对应的数据目录,把编写好的tick页面存放到目录下,进行刷新显示,我们就可以在右侧的资源路径复制tick页面的路径。

3.客户端页面改写

客户端我们只需要修正三个位置

  1. setLocation()改为执行tick页面

    javascript 复制代码
    // 模型位移-从当前位置位移到目标位置
    const setLocation = async arr => {
    	// 设置移动状态为true
    	moveState = true
    	const tickUrl = `@path:tick/moveTo.html?data=${JSON.stringify(arr)}&fps=${fps}&.html`
    	await fdapi.registerTick(tickUrl)
    }
  2. DigitalTwinPlayerapiOptions.onEvent 接受回调信息,把移动状态设置为false

    javascript 复制代码
    apiOptions: {
    	onEvent: e => {
    		if (e.Data === 'tick end') moveState = false
    	}
    }
  3. 重置模型时销毁tick

javascript 复制代码
// 重置模型
const reset = async () => {
	await fdapi.removeTick() // 加多一行
}

把这些实现完后,咱们的tick位移拆楼就完成了。

实现代码

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>tick位移拆楼</title>

    <link rel="stylesheet" href="./lib/styles.css" />

    <script type="text/javascript" src="./lib/aircity/ac_conf.js"></script>
    <script type="text/javascript" src="./lib/aircity/ac.min.js"></script>
  </head>
  <body>
    <div id="player"></div>

    <div class="btn" onclick="begin()">开始拆楼</div>
    <div class="btn" onclick="reset()" style="right: 220px">结束拆楼</div>

    <div class="build-list">
      <div class="build-item" onclick="chooseFloor('L13')">L13</div>
      <div class="build-item" onclick="chooseFloor('L12')">L12</div>
      <div class="build-item" onclick="chooseFloor('L11')">L11</div>
      <div class="build-item" onclick="chooseFloor('L10')">L10</div>
      <div class="build-item" onclick="chooseFloor('L9')">L9</div>
      <div class="build-item" onclick="chooseFloor('L8')">L8</div>
      <div class="build-item" onclick="chooseFloor('L7')">L7</div>
      <div class="build-item" onclick="chooseFloor('L6')">L6</div>
      <div class="build-item" onclick="chooseFloor('L5')">L5</div>
      <div class="build-item" onclick="chooseFloor('L1-4')">L1-4</div>
    </div>

    <script type="text/javascript">
      // 拆楼建筑名称数组
      const buildNameList = ['L1-4', 'L5', 'L6', 'L7', 'L8', 'L9', 'L10', 'L11', 'L12', 'L13']
      // 拆楼建筑对象,保存id与原始坐标信息
      const buildInfo = {}

      let openState = false, // 建筑开启状态
        openFloor = '', // 当前打开楼层
        moveState = false // 移动状态

      // 效果参数
      const fps = 20, // 动画需要多少帧完成
        risingHeight = 5, // 建筑抬升高度
        distance = 20, // 拆楼距离
        angle = 270 // 拆楼角度,0是x轴即正东方向,逆时针计算

      /**
       * 初始化场景
       */
      const OnLoad = () => {
        const __DigitalTwinPlayer = new DigitalTwinPlayer(HostConfig.Player, {
          domId: 'player',
          apiOptions: {
            onEvent: e => {
              if (e.Data === 'tick end') moveState = false
            },
            //事件监听回调函数
            onReady: async () => {
              // 重置视频流
              await fdapi.reset(2 | 4)
              await fdapi.removeTick()

              // 获取图层树信息
              const { infotree } = await fdapi.infoTree.get()
              // 把图层id存放到一个数组中,方便查询属性
              const idList = []
              infotree.forEach(item => {
                if (buildNameList.includes(item.name)) {
                  idList.push(item.iD)
                }
              })
              // 查询图层属性
              const { data } = await fdapi.tileLayer.get(idList)
              // 存储每个图层对应的属性
              data.forEach((item, index) => {
                buildInfo[buildNameList[index]] = {
                  id: item.id,
                  location: item.location,
                  openLocation: [item.location[0], item.location[1], item.location[2] + (index + 1) * risingHeight]
                }
              })
            }
          }
        })
      }

      // 开始拆楼
      const begin = () => {
        if (openState) {
          console.log('模型已经开启,可点击楼层进行拆楼')
          return
        }
        // 构造setLocationList id,对应结束的位置
        const setLocationList = []
        for (let key in buildInfo) {
          setLocationList.push({
            id: buildInfo[key].id,
            location: buildInfo[key].openLocation
          })
        }
        setLocation(setLocationList)

        // 打开状态设置为开启
        openState = true
      }

      // 点击某一层
      const chooseFloor = name => {
        if (!openState) {
          console.log('\x1b[33m%s\x1b[0m', '请先开启拆楼')
          return
        }

        if (moveState) {
          console.log('\x1b[33m%s\x1b[0m', '模型位移中,请等模型位移结束再操作')
          return
        }

        if (name === buildNameList[buildNameList.length - 1]) {
          console.log('\x1b[33m%s\x1b[0m', '顶层不拆')
          return
        }

        const setLocationList = []

        // 如果已经有打开的楼层,则要把当前楼层回收
        if (openFloor) {
          setLocationList.push({ id: [buildInfo[openFloor].id], location: buildInfo[openFloor].openLocation })

          // 同一层则直接重置当前楼层
          if (openFloor == name) {
            setLocation(setLocationList)
            openFloor = ''
            return
          }
        }

        // 需要位移的楼层:通过起点、角度、距离 计算终点位置
        const startLocation = JSON.parse(JSON.stringify(buildInfo[name].openLocation))
        const endLocation = calculatePoint(startLocation, angle, distance)
        setLocationList.push({ id: [buildInfo[name].id], location: endLocation })
        setLocation(setLocationList)
        openFloor = name // 保存当前楼层信息
      }

      // 模型位移-从当前位置位移到目标位置
      const setLocation = async arr => {
        // 设置移动状态为true
        moveState = true

        const tickUrl = `@path:tick/moveTo.html?data=${JSON.stringify(arr)}&fps=${fps}&.html`
        await fdapi.registerTick(tickUrl)
      }

      // 重置模型
      const reset = async () => {
        if (!openState) {
          console.log('\x1b[33m%s\x1b[0m', '模型不需要重置')
          return
        }

        await fdapi.removeTick()

        fdapi.tileLayer.updateBegin()
        for (let key in buildInfo) {
          fdapi.tileLayer.setLocation(buildInfo[key].id, buildInfo[key].location)
        }
        fdapi.tileLayer.updateEnd()

        openState = false
        openFloor = ''
      }

      // 根据角度和距离计算点
      function calculatePoint(point, angle, distance) {
        var radians = angle * (Math.PI / 180) // 将角度转换为弧度
        var x1 = point[0] + distance * Math.cos(radians)
        var y1 = point[1] + distance * Math.sin(radians)
        return point[2] ? [x1, y1, point[2]] : [x1, y1]
      }

      /**
       * 自适应分辨率
       */
      const onResize = async () => {
        let playerView = document.getElementById('player')
        playerView.style.height = window.innerHeight + 'px'
        playerView.style.width = window.innerWidth + 'px'
      }

      // 页面加载调用
      window.addEventListener('load', OnLoad, true)

      // 页面窗口变换调用
      window.addEventListener('resize', onResize, true)
    </script>
  </body>
</html>
相关推荐
API_technology几秒前
电商API安全防护:JWT令牌与XSS防御实战
前端·安全·xss
yqcoder5 分钟前
Express + MongoDB 实现在筛选时间段中用户名的模糊查询
java·前端·javascript
十八朵郁金香27 分钟前
通俗易懂的DOM1级标准介绍
开发语言·前端·javascript
GDAL1 小时前
HTML 中的 Canvas 样式设置全解
javascript
m0_528723812 小时前
HTML中,title和h1标签的区别是什么?
前端·html
Dark_programmer2 小时前
html - - - - - modal弹窗出现时,页面怎么能限制滚动
前端·html
GDAL2 小时前
HTML Canvas clip 深入全面讲解
前端·javascript·canvas
禾苗种树2 小时前
在 Vue 3 中使用 ECharts 制作多 Y 轴折线图时,若希望 **Y 轴颜色自动匹配折线颜色**且无需手动干预,可以通过以下步骤实现:
前端·vue.js·echarts
GISer_Jing2 小时前
Javascript排序算法(冒泡排序、快速排序、选择排序、堆排序、插入排序、希尔排序)详解
javascript·算法·排序算法
贵州数擎科技有限公司2 小时前
使用 Three.js 实现流光特效
前端·webgl