Recap of the previous chapter: Displaying a 3D space
- You learned what a Work is, and how to fetch and load one.
- How to display a 3D space, and to build a component on top of that to control the 3D space.
In this chapter you will learn
- What a State is.
- How to change the direction / position from which the 3D space is observed.
- How the code from the previous chapter — such as
useCurrentStateand other reactive pieces — works. - How to build an auto look-around feature using State.
Getting ready
Just like in the previous chapter, create a new directory (src/2.knowing-state) along with the corresponding html file and jsx or tsx files.
You can start the jsx or tsx files by copying the contents from the previous chapter.
<!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>Changing the Viewpoint | 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>import { useState, useEffect } from "react";
import { parseWork } from "@realsee/five";
/**
* React Hook: fetch a work object from the URL of a work.json
* @param url the data URL of work.json
* @returns the work object, or null while it is still loading
*/
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 };import { useState, useEffect } from "react";
/**
* Get the current window size
*/
function getWindowDimensions() {
return { width: window.innerWidth, height: window.innerHeight };
}
/**
* React Hook: get the current window size
*/
function useWindowDimensions() {
const [size, setSize] = useState(getWindowDimensions);
useEffect(() => {
const listener = () => setSize(getWindowDimensions());
window.addEventListener("resize", listener, false);
return () => window.removeEventListener("resize", listener, false);
});
return size;
}
export { useWindowDimensions };import React from "react";
import { Five } from "@realsee/five";
import { useFiveCurrentState } from "@realsee/five/react";
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";
/**
* React Component: mode controller
*/
const ModeController = () => {
const [state, setState] = useFiveCurrentState();
return <Paper sx={{ position: "fixed", bottom: 0, left: 0, right: 0 }}>
<BottomNavigation
showLabels
value={state.mode}
onChange={(_, newValue) => {
setState({ mode: newValue });
}}
>
<BottomNavigationAction label="Panorama Roaming" icon={<DirectionsWalkIcon/>} value={Five.Mode.Panorama}/>
<BottomNavigationAction label="Space Overview" icon={<ViewInArIcon/>} value={Five.Mode.Floorplan}/>
</BottomNavigation>
</Paper>;
}
export { ModeController };import React from "react";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { useFetchWork } from "./useFetchWork";
import { useWindowDimensions } from "./useWindowDimensions";
import { ModeController } from "./ModeController";
/** the data URL of work.json */
const workURL = "https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
const FiveProvider = createFiveProvider();
const App = () => {
const work = useFetchWork(workURL);
const size = useWindowDimensions();
return work && <FiveProvider initialWork={work}>
<FiveCanvas {...size}/>
<ModeController/>
</FiveProvider>;
};
export { App };import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";
ReactDOM.render(<App/>, document.querySelector("#app"));
export {};import { useState, useEffect } from "react";
import { Work, parseWork } from "@realsee/five";
/**
* React Hook: fetch a work object from the URL of a work.json
* @param url the data URL of work.json
* @returns the work object, or null while it is still loading
*/
function useFetchWork(url: string) {
const [work, setWork] = useState<Work | null>(null);
useEffect(() => {
setWork(null);
fetch(url)
.then(response => response.text())
.then(text => setWork(parseWork(text)));
},[url]);
return work;
}
export { useFetchWork };import { useState, useEffect } from "react";
/**
* Get the current window size
*/
function getWindowDimensions() {
return { width: window.innerWidth, height: window.innerHeight };
}
/**
* React Hook: get the current window size
*/
function useWindowDimensions() {
const [size, setSize] = useState(getWindowDimensions);
useEffect(() => {
const listener = () => setSize(getWindowDimensions());
window.addEventListener("resize", listener, false);
return () => window.removeEventListener("resize", listener, false);
});
return size;
}
export { useWindowDimensions };import React, { FC } from "react";
import { Five, Mode } from "@realsee/five";
import { useFiveCurrentState } from "@realsee/five/react";
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";
/**
* React Component: mode controller
*/
const ModeController: FC = () => {
const [state, setState] = useFiveCurrentState();
return <Paper sx={{ position: "fixed", bottom: 0, left: 0, right: 0 }}>
<BottomNavigation
showLabels
value={state.mode}
onChange={(_, newValue: Mode) => {
setState({ mode: newValue });
}}
>
<BottomNavigationAction label="Panorama Roaming" icon={<DirectionsWalkIcon/>} value={Five.Mode.Panorama}/>
<BottomNavigationAction label="Space Overview" icon={<ViewInArIcon/>} value={Five.Mode.Floorplan}/>
</BottomNavigation>
</Paper>;
}
export { ModeController };import React, { FC } from "react";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { useFetchWork } from "./useFetchWork";
import { useWindowDimensions } from "./useWindowDimensions";
/** the data URL of work.json */
const workURL = "https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
const FiveProvider = createFiveProvider();
const App: FC = () => {
const work = useFetchWork(workURL);
const size = useWindowDimensions();
return work && <FiveProvider initialWork={work}>
<FiveCanvas {...size}/>
</FiveProvider>;
};
export { App };import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";
ReactDOM.render(<App/>, document.querySelector("#app"));
export {};Start the dev server with npm run dev, then navigate to the current page at "http://localhost:3000/src/2.knowing-state/index.html".
Info
Check your console — the port number may change depending on your configuration and which ports are currently in use, so always rely on what the console prints. If you use a different build/dev tool, start the server according to that tool's instructions.
What is State
Time for some concepts again. I promise this is the last bit of theory you need at this stage.
Introduction to State
State is the data structure used to describe a status. In the previous chapter we learned about Work: Work describes a 3D space, while State describes what status that 3D space is currently in. It carries information about the mode, the capture point you are standing on, the camera direction, and the camera's field of view.
The State data structure and its fields
interface State {
"mode": Five.Mode,
"panoIndex": number,
"longitude": number,
"latitude": number,
"fov": number,
"offset": THREE.Vector3
}Description of the State data:
mode: the current mode.
Five SDK commonly has 5 modes, which you can obtain via Five.Mode:
- Panorama: panorama roaming mode. In this mode the view walks between capture points; gestures let you rotate the view / zoom in / switch capture points. Suitable for viewing the captured panorama information.
- Floorplan: space overview mode. In this mode the view is centered on the model; gestures let you rotate / zoom the model / switch floors. Suitable for viewing the overall look of the model.
- Topview: floor-plan mode. In this mode the view is centered on the model and looks straight down on it; gestures let you pan / zoom the model / switch floors. Suitable for viewing the model's floor-plan structure.
- Model: model roaming mode. In this mode the view roams freely through the model; gestures let you rotate the view / zoom / move around. Suitable for examining model details and performing positioning operations.
- VRPanorama: VR-headset mode. In this mode you can use Cardboard goggles or their third-party derivatives to achieve a VR virtual-display effect.
panoIndex: the capture point — that is, a position you can land on in Panorama mode. It is an index intowork[observers].longitude/latitude: the camera's horizontal angle (yaw) / the camera's vertical angle (pitch), in radians. We describe the camera position using a latitude/longitude-like scheme.
The whole model scene is a right-handed Cartesian coordinate system, where the XZ plane is parallel to the ground and the Y axis is perpendicular to the ground.
The initial camera direction is from the origin looking toward the negative Z axis.
- Increasing
longituderotates the camera left. - Decreasing
longituderotates the camera right. - Increasing
latituderotates the camera down. - Decreasing
latituderotates the camera up.
fov: the camera's vertical field of view (in degrees).offset: the camera's current 3D coordinate.
Which APIs are related to state
- [
[state, setState] = useFiveState;](https://unpkg.com/@realsee/five@6.8.9/docs/functions/react.useFiveState.html) - [
[currentState, setCurrentState] = useFiveCurrentState;](https://unpkg.com/@realsee/five@6.8.9/docs/functions/react.useFiveCurrentState.html)
You can read the current state via state / currentState, and set the state via setState / setCurrentState.
The difference between state and currentState
currentState is the current state — the state on screen, the status being displayed right now. state is the target state, or in other words the stable state at the next moment in time.
You can think of it simply like this:
When setState is called, state immediately becomes the value passed to setState, whereas currentState does not change right away — it gradually approaches state over the course of the transition animation and eventually becomes the same value as state. Just like the animation you see on screen.
In the previous chapter's code example, we already used the mode property to switch between the Panorama and Floorplan modes. You can also try adding the other modes and see how each one differs.
VRPanorama mode relies on the device's gyroscope information, so it requires a mobile device.
Also, on iOS devices the service must be served over
https; otherwise iOS will not allow access to gyroscope information.
Building an auto look-around feature
We have already read and set mode; this time let's try changing longitude / latitude. Here we will build an auto look-around feature. A button toggles the auto look-around on, and the feature automatically rotates the camera horizontally.
Writing the look-around component
- Add a LookAroundController file to hold the component.
- Design an active React state to control whether the look-around feature is enabled.
- The look-around feature is implemented by periodically triggering a function with setInterval that modifies Five SDK's state.
import React, { useState, useEffect } from "react";
import { useFiveCurrentState } from "@realsee/five/react";
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";
/**
* ReactComponent: auto look-around button
*/
const LookAroundController = () => {
const [currentState, setState] = useFiveCurrentState();
const [active, toggleActive] = useState(false);
useEffect(() => {
if (active) {
const timer = window.setInterval(() => {
setState(prevState => {
return { longitude: prevState.longitude + Math.PI / 360 };
});
}, 16);
return () => window.clearInterval(timer);
}
}, [active]);
return <Paper sx={{ position: "fixed", top: 10, right: 10 }}>
{active ?
<IconButton onClick={() => toggleActive(false)}><PauseIcon/></IconButton>:
<IconButton onClick={() => toggleActive(true)}><FlipCameraAndroidIcon/></IconButton>
}
</Paper>;
}
export { LookAroundController };import React, { FC, useState, useEffect } from "react";
import { useFiveCurrentState } from "@realsee/five/react";
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";
/**
* ReactComponent: auto look-around button
*/
const LookAroundController: FC = () => {
const [currentState, setState] = useFiveCurrentState();
const [active, toggleActive] = useState(false);
useEffect(() => {
if (active) {
const timer = window.setInterval(() => {
setState(prevState => {
return { longitude: prevState.longitude + Math.PI / 360 };
});
}, 16);
return () => window.clearInterval(timer);
}
}, [active]);
return <Paper sx={{ position: "fixed", top: 10, right: 10 }}>
{active ?
<IconButton onClick={() => toggleActive(false)}><PauseIcon/></IconButton>:
<IconButton onClick={() => toggleActive(true)}><FlipCameraAndroidIcon/></IconButton>
}
</Paper>;
}
export { LookAroundController };Using the auto look-around component
Insert it inside the FiveProvider in the App file.
import React from "react";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { useFetchWork } from "./useFetchWork";
import { useWindowDimensions } from "./useWindowDimensions";
import { ModeController } from "./ModeController";
// highlight-start
import { LookAroundController } from "./LookAroundController";
// highlight-end
/** the data URL of work.json */
const workURL = "https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
const FiveProvider = createFiveProvider();
const App = () => {
const work = useFetchWork(workURL);
const size = useWindowDimensions();
return work && <FiveProvider initialWork={work}>
<FiveCanvas {...size}/>
<ModeController/>
// highlight-start
<LookAroundController/>
// highlight-end
</FiveProvider>;
};
export { App };import React, { FC } from "react";
import { createFiveProvider, FiveCanvas } from "@realsee/five/react";
import { useFetchWork } from "./useFetchWork";
import { useWindowDimensions } from "./useWindowDimensions";
import { ModeController } from "./ModeController";
// highlight-start
import { LookAroundController } from "./LookAroundController";
// highlight-end
/** the data URL of work.json */
const workURL = "https://vr-public.realsee-cdn.cn/release/static/image/release/five/work-sample/07bdc58f413bc5494f05c7cbb5cbdce4/work.json";
const FiveProvider = createFiveProvider();
const App: FC = () => {
const work = useFetchWork(workURL);
const size = useWindowDimensions();
return work && <FiveProvider initialWork={work}>
<FiveCanvas {...size}/>
<ModeController/>
// highlight-start
<LookAroundController/>
// highlight-end
</FiveProvider>;
};
export { App };Go back to your browser and you will see a look-around button appear in the top-right corner of the page. Clicking it toggles the look-around on and off.
What a nice feature 🥳!
What you will learn in the next chapter
In the next chapter we will use **State** to build some more complex features and get a deeper feel for what **State** can do.
- Record user operations using State.
- Replay the on-screen user operations using State.
