改变视角
上一章回顾: 展示三维空间
- 你了解了什么是 Work, 以及如何获取和加载。
 - 如何展示三维空间,并且再次基础上开发组件来控制三维空间。
 
- 了解什么是 State。
 - 如何改变三维空间观察的方向 / 位置。
 - 了解上一章的代码,比如 
currentStatesetState及其他响应的是如何工作的。 - 通过 State 完成了自动环视的功能。
 
准备工作
和上一章节一样,我们新建一个目录(src/2.knowing-state)以及对应的 html 文件 以及 jsx 或 tsx 文件。
jsx 或 tsx 文件可以先拷贝上一章节的内容。
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link rel="icon" href="data:;base64,iVBORw0KGgo=">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>改变视角 | Knowing state</title>
  <style>
    * { margin: 0; padding: 0; }
    html, body #app { width: 100%; height: 100%; overflow: hidden; }
  </style>
</head>
<body>
  <div id="app"></div>
  <script type="module" src="./index"></script>
</body>
</html>
- JavaScript
 - TypeScript
 
src/2.knowing-state/withFetchWork.jsx
import { useState, useEffect } from "react";
import { parseWork } from "@realsee/five";
/**
 * React Hook: 通过 work.json 的地址 获取 work 对象
 * @param url work.json 的数据地址
 * @returns work 对象,如果获取中,返回 null
 */
 function useFetchWork(url) {
  const [work, setWork] = useState(null);
  useEffect(() => {
    setWork(null);
    fetch(url)
      .then(response => response.text())
      .then(text => setWork(parseWork(text)));
  },[url]);
  return work;
}
export { useFetchWork };
src/2.knowing-state/withWindowDimensions.js
import React, { Component } from "react";
/**
 * React HOC: 获取当前窗口的尺寸
 */
function withWindowDimensions() {
  return function(Compnent) {
    return class extends Component {
      state = this.getWindowDimensions();
      resizeListener = () => {
        this.setState(this.getWindowDimensions());
      };
      getWindowDimensions() {
        return { width: window.innerWidth, height: window.innerHeight };
      }
      componentDidMount() {
        window.addEventListener("resize", this.resizeListener, false);
      }
      componentWillUnmount() {
        window.removeEventListener("resize", this.resizeListener, false);
      }
      render() {
        const dimensions = { width: this.state.width, height: this.state.height };
        return <Compnent windowDimensions={dimensions} {...this.props}/>;
      }
    }
  }
}
export { withWindowDimensions };
src/2.knowing-state/ModeController.jsx
import React, { Component } from "react";
import { Five } from "@realsee/five";
import { withFive, createFiveFeature } from "@realsee/five/react";
import { compose } from "@wordpress/compose";
import BottomNavigation from "@mui/material/BottomNavigation";
import BottomNavigationAction from "@mui/material/BottomNavigationAction";
import Paper from "@mui/material/Paper";
import DirectionsWalkIcon from "@mui/icons-material/DirectionsWalk";
import ViewInArIcon from "@mui/icons-material/ViewInAr";
const FEATURES = createFiveFeature("currentState", "setState");
/**
 * React Component: 模态控制
 */
const ModeController = compose(
  withFive(FEATURES)
)(class extends Component {
  render() {
    return <Paper sx={{ position: "fixed", bottom: 0, left: 0, right: 0 }}>
      <BottomNavigation
        showLabels
        value={this.props.$five.currentState.mode}
        onChange={(_, newValue) => {
          this.props.$five.setState({ mode: newValue });
        }}
      >
        <BottomNavigationAction label="全景漫游" icon={<DirectionsWalkIcon/>} value={Five.Mode.Panorama}/>
        <BottomNavigationAction label="空间总览" icon={<ViewInArIcon/>} value={Five.Mode.Floorplan}/>
      </BottomNavigation>
    </Paper>;
  }
})
export { ModeController };
src/2.knowing-state/App.jsx
import React, { Component } from "react";
import { compose } from "@wordpress/compose";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { withFetchWork } from "./withFetchWork";
import { withWindowDimensions } from "./withWindowDimensions";
import { ModeController } from "./ModeController";
/** work.json 的数据 URL */
const workURL = "https://vrlab-public.ljcdn.com/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
const FiveProvider = createFiveProvider();
const App = compose(
  withFetchWork(workURL),
  withWindowDimensions()
)(class extends Component {
  render() {
    const { work, windowDimensions } = this.props;
    return <FiveProvider initialWork={work}>
      <FiveCanvas width={windowDimensions.width} height={windowDimensions.height}/>
      <ModeController/>;
    </FiveProvider>;
  }
});
export { App };
src/2.knowing-state/index.jsx
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";
ReactDOM.render(<App/>, document.querySelector("#app"));
export {};
src/2.knowing-state/withFetchWork.tsx
import React, { Component, ComponentClass } from "react";
import { Work, parseWork } from "@realsee/five";
/**
 * React HOC 获取 work
 * @param url work.json 的地址
 */
function withFetchWork<P extends Record<string, any>>(url: string) {
  return function(Compnent: ComponentClass<P & { work: Work }>): ComponentClass<P> {
    return class extends Component<P, {work: Work | null}> {
      state = { work: null };
      componentDidMount() {
        fetch(url).then(res => res.json()).then(json => {
          this.setState({ work: parseWork(json) });
        });
      }
      render() {
        if (this.state.work === null) return null;
        return <Compnent work={this.state.work} {...this.props}/>;
      }
    }
  }
}
export { withFetchWork };
src/2.knowing-state/useWindowDimensions.tsx
import React, { Component, ComponentClass } from "react";
/**
 * React HOC: 获取当前窗口的尺寸
 */
function withWindowDimensions<P extends Record<string, any>>() {
  return function(Compnent: ComponentClass<P & { windowDimensions: { width: number, height: number} }>): ComponentClass<P> {
    return class extends Component<P, {width: number, height: number}> {
      state = this.getWindowDimensions();
      resizeListener = () => {
        this.setState(this.getWindowDimensions());
      };
      getWindowDimensions() {
        return { width: window.innerWidth, height: window.innerHeight };
      }
      componentDidMount() {
        window.addEventListener("resize", this.resizeListener, false);
      }
      componentWillUnmount() {
        window.removeEventListener("resize", this.resizeListener, false);
      }
      render() {
        const dimensions = { width: this.state.width, height: this.state.height };
        return <Compnent windowDimensions={dimensions} {...this.props}/>;
      }
    }
  }
}
export { withWindowDimensions };
src/2.knowing-state/ModeController.tsx
import React, { Component } from "react";
import { Five, Mode } from "@realsee/five";
import { withFive, createFiveFeature, PropTypeOfFiveFeatures } from "@realsee/five/react";
import { compose } from "@wordpress/compose";
import BottomNavigation from "@mui/material/BottomNavigation";
import BottomNavigationAction from "@mui/material/BottomNavigationAction";
import Paper from "@mui/material/Paper";
import DirectionsWalkIcon from "@mui/icons-material/DirectionsWalk";
import ViewInArIcon from "@mui/icons-material/ViewInAr";
const FEATURES = createFiveFeature("currentState", "setState");
type Props = PropTypeOfFiveFeatures<typeof FEATURES>;
/**
 * React Component: 模态控制
 */
const ModeController = compose(
  withFive(FEATURES)
)(class extends Component<Props> {
  render() {
    return <Paper sx={{ position: "fixed", bottom: 0, left: 0, right: 0 }}>
      <BottomNavigation
        showLabels
        value={this.props.$five.currentState.mode}
        onChange={(_, newValue: Mode) => {
          this.props.$five.setState({ mode: newValue });
        }}
      >
        <BottomNavigationAction label="全景漫游" icon={<DirectionsWalkIcon/>} value={Five.Mode.Panorama}/>
        <BottomNavigationAction label="空间总览" icon={<ViewInArIcon/>} value={Five.Mode.Floorplan}/>
      </BottomNavigation>
    </Paper>;
  }
})
export { ModeController };
src/2.knowing-state/App.tsx
import React, { Component } from "react";
import { Work } from "@realsee/five";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { compose } from "@wordpress/compose";
import { withFetchWork } from "./withFetchWork";
import { withWindowDimensions } from "./withWindowDimensions";
import { ModeController } from "./ModeController";
/** work.json 的数据 URL */
const workURL = "https://vrlab-public.ljcdn.com/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
const FiveProvider = createFiveProvider();
const App = compose(
  withFetchWork(workURL),
  withWindowDimensions()
)(class extends Component<{work: Work, windowDimensions: { width: number, height: number }}> {
  render() {
    const { work, windowDimensions } = this.props;
    return <FiveProvider initialWork={work}>
      <FiveCanvas width={windowDimensions.width} height={windowDimensions.height}/>
      <ModeController/>
    </FiveProvider>;
  }
});
export { App };
src/2.knowing-state/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";
ReactDOM.render(<App/>, document.querySelector("#app"));
export {};
启动服务 npm run dev。 并跳转到当前页面 "http://localhost:3000/src/2.knowing-state/index.html"。
请查看你的控制台,端口号会因为你的配置以及当前端口占用情况变更,请已控制台输出的为准。 如果你使用其他开发构建工具,请按照自己的开发构建工具的要求启动服务。
什么是 State
由来了解概念了,我保证着你是现阶段需要了解的最后一个理论只是了。
State 简介
State 用于描述状态的数据结构。上一章我们了解了 Work, Work 是用于描述一个三维空间,那么 State 则是描述在这个三维空间内,目前正在处于什么状态。他包含了模态、位于的采集点位、相机的方向、相机可视角度的信息。
State 的数据结构及说明
interface State {
  "mode": Five.Mode,
  "panoIndex": number,
  "longitude": number,
  "latitude": number,
  "fov": number,
  "offset": THEEE.Vector3
}
State的数据描述
mode: 当前的模态 Five 常用有 5 种模态,可以使用Five.Mode获得- Panorama: 全景游走模态,该模态下视图将在采集点间游走,手势操作可以旋转/放大视角/切换采集点,适合查看采集的全景信息。
 - Floorplan: 空间总览模态, 该模态下视图以模型为中心,手势操作可以旋转/放大模型/切换楼层,适合查看模型的整体效果。
 - Topview: 户型图模态,该模态下视图以模型为中心,垂直俯视模型,手势操作可以平移/放大模型/切换楼层,适合查看模型平面结构。
 - Model: 模型游走模态,该模态下视图将在模型中自由游走,手势操作可以旋转/放大视角/位移,适合查看模型的细节,做一些定位操作。
 - VRPanorama: VR 眼镜模态,该模态下可以使用 Cardboard 眼镜 或者他的第三方衍生产品,实现 VR 虚拟显示效果。
 
panoIndex: 采集点位,即在 Panorama 模态可以落脚的位置。是一个work[observers]的一个索引。longitude/latitude: 相机的水平角 / 相机的偏航角(弧度),我们使用类似经纬度的方式描述相机位置。整个模型场景为一个右手笛卡尔坐标系,
XZ平面平行于地面,Y轴垂直于地面。初始相机方向为原点看向 Z 轴负方向。
- 增加 
longitude相机向左旋转。 - 减少 
longitude相机向右旋转。 - 增加 
latitude相机向下旋转。 - 减少 
latitude相机向上旋转。 
- 增加 
 
fov: 相机垂直方向的可视角度 (角度)。offset: 当前相机的三维坐标。
state 相关的 API 有什么
可以通过 state currentState 获取当前状态,通过 setState setCurrentState 设置状态。
state / currentState 的区别
currentState 是当前状态,就是画面中的状态,目前展示的状态。 state 是目标状态或者说下一时间的稳定状态。
可以简单的这样认为: 
当 setSate 被调用后,state 当即会变成 setState 的值,而 currentState 则不会马上改变,他会在动画过渡的过程中逐步逼近 state 并最终变成和  state 一样的值。就和你在画面中看到的动画一样。
在上一章的代码示例中,我们已经使用了 mode 属性,来切换 Panorama 以及 Floorplan 模态。你也可以尝试把其他的几种模态都补充上,看看各种模态下有什么区别。
VRPanorama 模态需要使用设备陀螺仪信息,需要使用移动设备。 并且如果在 iOS 设备下,需要服务处于
https下,否则 iOS 不允许访问陀螺仪信息。
开发一个自动环视的功能
我们已经获取和设置过了 mode,这次我们修改下 longitude / latitude 试试。这次我们开发一个自动环视的功能。通过一个按钮控制激活自动环视的功能,该功能会自动水平旋转镜头。
书写环视组件
- 添加一个 LookAroundController 文件,用于编写组件。
 - 设计 active React state 用于控制环视的功能是否开启的按钮展示状态。
 - 环视功能的实现为:通过 setInterval 定时触发函数,修改 five 的 state。
 
- JavaScript
 - TypeScript
 
import React, { Component } from "react";
import { withFive, createFiveFeature } from "@realsee/five/react";
import { compose } from "@wordpress/compose";
import IconButton from "@mui/material/IconButton";
import Paper from "@mui/material/Paper";
import FlipCameraAndroidIcon from "@mui/icons-material/FlipCameraAndroid";
import PauseIcon from "@mui/icons-material/Pause";
const FEATURES = createFiveFeature("currentState", "setState");
/**
 * ReactComponent: 自动环视按钮
 */
const LookAroundController = compose(
  withFive(FEATURES)
)(class extends Component {
  timer;
  state = { active: false };
  toggleActive(active) {
    window.clearInterval(this.timer);
    this.setState({ active });
    if (active === true) {
      this.timer = window.setInterval(() => {
        this.props.$five.setState({
          longitude: this.props.$five.currentState.longitude + Math.PI / 360
        });
      }, 16);
    } else {
      delete this.timer;
    }
  }
  render() {
    return <Paper sx={{ position: "fixed", top: 10, right: 10 }}>
    {this.state.active ?
      <IconButton onClick={() => this.toggleActive(false)}><PauseIcon/></IconButton>:
      <IconButton onClick={() => this.toggleActive(true)}><FlipCameraAndroidIcon/></IconButton>
    }
    </Paper>;
  }
});
export { LookAroundController };
import React, { Component } from "react";
import { withFive, createFiveFeature, PropTypeOfFiveFeatures } from "@realsee/five/react";
import { compose } from "@wordpress/compose";
import IconButton from "@mui/material/IconButton";
import Paper from "@mui/material/Paper";
import FlipCameraAndroidIcon from "@mui/icons-material/FlipCameraAndroid";
import PauseIcon from "@mui/icons-material/Pause";
const FEATURES = createFiveFeature("currentState", "setState");
type Props = PropTypeOfFiveFeatures<typeof FEATURES>;
type State = { active: boolean };
/**
 * ReactComponent: 自动环视按钮
 */
const LookAroundController = compose(
  withFive(FEATURES)
)(class extends Component<Props, State> {
  timer?: number;
  state = { active: false };
  toggleActive(active: boolean) {
    window.clearInterval(this.timer);
    this.setState({ active });
    if (active === true) {
      this.timer = window.setInterval(() => {
        this.props.$five.setState({
          longitude: this.props.$five.currentState.longitude + Math.PI / 360
        });
      }, 16);
    } else {
      delete this.timer;
    }
  }
  render() {
    return <Paper sx={{ position: "fixed", top: 10, right: 10 }}>
    {this.state.active ?
      <IconButton onClick={() => this.toggleActive(false)}><PauseIcon/></IconButton>:
      <IconButton onClick={() => this.toggleActive(true)}><FlipCameraAndroidIcon/></IconButton>
    }
    </Paper>;
  }
});
export { LookAroundController };
使用自动环视组件
插入到 App 文件的 FiveProvider 中
- JavaScript
 - TypeScript
 
import React, { Component } from "react";
import { compose } from "@wordpress/compose";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { withFetchWork } from "./withFetchWork";
import { withWindowDimensions } from "./withWindowDimensions";
import { ModeController } from "./ModeController";
import { LookAroundController } from "./LookAroundController";
/** work.json 的数据 URL */
const workURL = "https://vrlab-public.ljcdn.com/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
const FiveProvider = createFiveProvider();
const App = compose(
  withFetchWork(workURL),
  withWindowDimensions()
)(class extends Component {
  render() {
    const { work, windowDimensions } = this.props;
    return <FiveProvider initialWork={work}>
      <FiveCanvas width={windowDimensions.width} height={windowDimensions.height}/>
      <ModeController/>;
      <LookAroundController/>;
    </FiveProvider>;
  }
});
export { App };
import React, { Component } from "react";
import { Work } from "@realsee/five";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { compose } from "@wordpress/compose";
import { withFetchWork } from "./withFetchWork";
import { withWindowDimensions } from "./withWindowDimensions";
import { ModeController } from "./ModeController";
import { LookAroundController } from "./LookAroundController";
/** work.json 的数据 URL */
const workURL = "https://vrlab-public.ljcdn.com/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
const FiveProvider = createFiveProvider();
const App = compose(
  withFetchWork(workURL),
  withWindowDimensions()
)(class extends Component<{work: Work, windowDimensions: { width: number, height: number }}> {
  render() {
    const { work, windowDimensions } = this.props;
    return <FiveProvider initialWork={work}>
      <FiveCanvas width={windowDimensions.width} height={windowDimensions.height}/>
      <ModeController/>
      <LookAroundController/>
    </FiveProvider>;
  }
});
export { App };
回到你的浏览器查看,会发现你的页面右上角出现一个环视按钮。点击可以触发和关闭环视。
真是一个不错的功能🥳 !
下一章节你会学到
- 通过 State 完成用户操作的录制
 - 通过 State 还原用户操作画面