Skip to content

Commit

Permalink
feat: add navigator as a built-in widget (#323)
Browse files Browse the repository at this point in the history
* feat: add navigator icon

* feat: link with cesium api for navigator

* feat: add navigator component as builtin widget

* test: fix test

* test: fix storybook

* Update src/components/molecules/Visualizer/Widget/Navigator/hooks.ts

Co-authored-by: rot1024 <[email protected]>

* refactor: move navigator icon

* refactor: rename to useHooks

* refactor: move Navigator to under Visualizer/Widget/Navigator

* test: add test for useEngineRef

* fix: switch position of plus button to minus button position

Co-authored-by: rot1024 <[email protected]>
  • Loading branch information
keiya01 and rot1024 authored Oct 3, 2022
1 parent 4e78524 commit 3befd4c
Show file tree
Hide file tree
Showing 23 changed files with 547 additions and 108 deletions.
1 change: 1 addition & 0 deletions src/components/atoms/Icon/Icons/widgetNavigator.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/components/atoms/Icon/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ import PlayLeft from "./Icons/play-left.svg";
import Ellipse from "./Icons/ellipse.svg";

// Navigator
import WidgetNavigator from "./Icons/widgetNavigator.svg";
import NavigatorAngle from "./Icons/navigatorAngle.svg";
import Compass from "./Icons/compass.svg";
import CompassFocus from "./Icons/compassFocus.svg";
Expand Down Expand Up @@ -270,6 +271,7 @@ export default {
playRight: PlayRight,
playLeft: PlayLeft,
timeline: Timeline,
navigator: WidgetNavigator,
navigatorAngle: NavigatorAngle,
compass: Compass,
compassFocus: CompassFocus,
Expand Down
64 changes: 63 additions & 1 deletion src/components/molecules/Visualizer/Engine/Cesium/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@ import {
Cartographic,
EllipsoidTerrainProvider,
sampleTerrainMostDetailed,
Ray,
IntersectionTests,
} from "cesium";
import { useCallback, MutableRefObject } from "react";

import { useCanvas, useImage } from "@reearth/util/image";
import { tweenInterval } from "@reearth/util/raf";
import { Camera } from "@reearth/util/value";

import { Clock } from "../../Plugin/types";
import { CameraOptions, Clock } from "../../Plugin/types";

export const layerIdField = `__reearth_layer_id`;

Expand Down Expand Up @@ -307,6 +309,66 @@ export const animateFOV = ({
return undefined;
};

/**
* Get the center of globe.
*/
export const getCenterCamera = ({
camera,
scene,
}: {
camera: CesiumCamera;
scene: Scene;
}): Cartesian3 | void => {
const result = new Cartesian3();
const ray = camera.getPickRay(camera.positionWC);
if (ray) {
ray.origin = camera.positionWC;
ray.direction = camera.directionWC;
return scene.globe.pick(ray, scene, result);
}
};

export const zoom = (
{ camera, scene, relativeAmount }: { camera: CesiumCamera; scene: Scene; relativeAmount: number },
options?: CameraOptions,
) => {
const center = getCenterCamera({ camera, scene });
const target =
center ||
IntersectionTests.grazingAltitudeLocation(
// Get the ray from cartographic to the camera direction
new Ray(
// Get the cartographic position of camera on 3D space.
scene.globe.ellipsoid.cartographicToCartesian(camera.positionCartographic),
// Get the camera direction.
camera.directionWC,
),
scene.globe.ellipsoid,
);

if (!target) {
return;
}

const orientation = {
heading: camera.heading,
pitch: camera.pitch,
roll: camera.roll,
};

const cartesian3Scratch = new Cartesian3();
const direction = Cartesian3.subtract(camera.position, target, cartesian3Scratch);
const movementVector = Cartesian3.multiplyByScalar(direction, relativeAmount, direction);
const endPosition = Cartesian3.add(target, movementVector, target);

camera.flyTo({
destination: endPosition,
orientation: orientation,
duration: options?.duration || 0.5,
convert: false,
});
};

export const getCamera = (viewer: Viewer | CesiumWidget | undefined): Camera | undefined => {
if (!viewer || viewer.isDestroyed() || !viewer.camera || !viewer.scene) return undefined;
const { camera } = viewer;
Expand Down
156 changes: 144 additions & 12 deletions src/components/molecules/Visualizer/Engine/Cesium/useEngineRef.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,30 @@ import {
Cartesian3,
Globe,
Ellipsoid,
Matrix4,
} from "cesium";
import { useRef } from "react";
import type { CesiumComponentRef } from "resium";
import { vi, expect, test } from "vitest";
import { vi, expect, test, afterEach } from "vitest";

import { Clock } from "../../Plugin/types";
import { EngineRef } from "../ref";

import useEngineRef from "./useEngineRef";

vi.mock("./common", async () => {
const commons: Record<string, any> = await vi.importActual("./common");
return {
...commons,
zoom: vi.fn(),
getCenterCamera: vi.fn(({ scene }) => scene?.globe?.pick?.()),
};
});

afterEach(() => {
vi.clearAllMocks();
});

test("engine should be cesium", () => {
const { result } = renderHook(() => {
const cesium = useRef<CesiumComponentRef<CesiumViewer>>(null);
Expand Down Expand Up @@ -157,17 +171,12 @@ test("requestRender", () => {
expect(mockRequestRender).toHaveBeenCalledTimes(1);
});

const mockZoomIn = vi.fn(amount => amount);
const mockZoomOut = vi.fn(amount => amount);
test("zoom", () => {
test("zoom", async () => {
const { result } = renderHook(() => {
const cesium = useRef<CesiumComponentRef<CesiumViewer>>({
cesiumElement: {
scene: {
camera: {
zoomIn: mockZoomIn,
zoomOut: mockZoomOut,
},
camera: {},
},
isDestroyed: () => {
return false;
Expand All @@ -179,13 +188,136 @@ test("zoom", () => {
return engineRef;
});

const commons = await import("./common");

result.current.current?.zoomIn(10);
expect(mockZoomIn).toHaveBeenCalledTimes(1);
expect(mockZoomIn).toHaveBeenCalledWith(10);
expect(commons.zoom).toHaveBeenCalledTimes(1);
expect(commons.zoom).toHaveBeenCalledWith(
{
camera: {},
scene: { camera: {} },
relativeAmount: 0.1,
},
undefined,
);

result.current.current?.zoomOut(20);
expect(mockZoomOut).toHaveBeenCalledTimes(1);
expect(mockZoomOut).toHaveBeenCalledWith(20);
expect(commons.zoom).toHaveBeenCalledTimes(2);
expect(commons.zoom).toHaveBeenCalledWith(
{
camera: {},
scene: { camera: {} },
relativeAmount: 20,
},
undefined,
);
});

test("call orbit when camera focuses on center", async () => {
const { result } = renderHook(() => {
const cesiumElement = {
scene: {
camera: { lookAtTransform: vi.fn(), rotateLeft: vi.fn(), rotateUp: vi.fn(), look: vi.fn() },
globe: {
ellipsoid: new Ellipsoid(),
pick: () => new Cartesian3(),
},
},
transform: new Matrix4(),
positionWC: new Cartesian3(),
isDestroyed: () => {
return false;
},
} as any;
const cesium = useRef<CesiumComponentRef<CesiumViewer>>({
cesiumElement,
});
const engineRef = useRef<EngineRef>(null);
useEngineRef(engineRef, cesium);
return [engineRef, cesium] as const;
});

const commons = await import("./common");

const [engineRef, cesium] = result.current;

engineRef.current?.orbit(90);
expect(commons.getCenterCamera).toHaveBeenCalled();
expect(cesium.current.cesiumElement?.scene.camera.rotateLeft).toHaveBeenCalled();
expect(cesium.current.cesiumElement?.scene.camera.rotateUp).toHaveBeenCalled();
expect(cesium.current.cesiumElement?.scene.camera.lookAtTransform).toHaveBeenCalledTimes(2);
});

test("call orbit when camera does not focus on center", async () => {
const { result } = renderHook(() => {
const cesiumElement = {
scene: {
camera: {
lookAtTransform: vi.fn(),
rotateLeft: vi.fn(),
rotateUp: vi.fn(),
look: vi.fn(),
positionWC: new Cartesian3(),
},
globe: {
ellipsoid: new Ellipsoid(),
pick: () => undefined,
},
},
transform: new Matrix4(),
isDestroyed: () => {
return false;
},
} as any;
const cesium = useRef<CesiumComponentRef<CesiumViewer>>({
cesiumElement,
});
const engineRef = useRef<EngineRef>(null);
useEngineRef(engineRef, cesium);
return [engineRef, cesium] as const;
});

const commons = await import("./common");

const [engineRef, cesium] = result.current;

engineRef.current?.orbit(90);
expect(commons.getCenterCamera).toHaveBeenCalled();
expect(cesium.current.cesiumElement?.scene.camera.look).toHaveBeenCalledTimes(2);
expect(cesium.current.cesiumElement?.scene.camera.lookAtTransform).toHaveBeenCalledTimes(2);
});

test("rotateRight", async () => {
const { result } = renderHook(() => {
const cesiumElement = {
scene: {
camera: {
lookAtTransform: vi.fn(),
rotateRight: vi.fn(),
positionWC: new Cartesian3(),
},
globe: {
ellipsoid: new Ellipsoid(),
},
},
transform: new Matrix4(),
isDestroyed: () => {
return false;
},
} as any;
const cesium = useRef<CesiumComponentRef<CesiumViewer>>({
cesiumElement,
});
const engineRef = useRef<EngineRef>(null);
useEngineRef(engineRef, cesium);
return [engineRef, cesium] as const;
});

const [engineRef, cesium] = result.current;

engineRef.current?.rotateRight(90);
expect(cesium.current.cesiumElement?.scene.camera.rotateRight).toHaveBeenCalled();
expect(cesium.current.cesiumElement?.scene.camera.lookAtTransform).toHaveBeenCalledTimes(2);
});

test("getClock", () => {
Expand Down
60 changes: 56 additions & 4 deletions src/components/molecules/Visualizer/Engine/Cesium/useEngineRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
moveRight,
moveOverTerrain,
flyToGround,
getCenterCamera,
zoom,
} from "./common";

export default function useEngineRef(
Expand Down Expand Up @@ -98,15 +100,65 @@ export default function useEngineRef(
}
: undefined;
},
zoomIn: amount => {
zoomIn: (amount, options) => {
const viewer = cesium.current?.cesiumElement;
if (!viewer || viewer.isDestroyed()) return;
viewer.scene?.camera.zoomIn(amount);
const scene = viewer.scene;
const camera = scene.camera;
zoom({ camera, scene, relativeAmount: 1 / amount }, options);
},
zoomOut: amount => {
zoomOut: (amount, options) => {
const viewer = cesium.current?.cesiumElement;
if (!viewer || viewer.isDestroyed()) return;
viewer?.scene?.camera.zoomOut(amount);
const scene = viewer.scene;
const camera = scene.camera;
zoom({ camera, scene, relativeAmount: amount }, options);
},
orbit: radian => {
const viewer = cesium.current?.cesiumElement;
if (!viewer || viewer.isDestroyed()) return;
const scene = viewer.scene;
const camera = scene.camera;

const distance = 0.02;
const angle = radian + CesiumMath.PI_OVER_TWO;

const x = Math.cos(angle) * distance;
const y = Math.sin(angle) * distance;

const oldTransform = Cesium.Matrix4.clone(camera.transform);

const center = getCenterCamera({ camera, scene });
// Get fixed frame from center to globe ellipsoid.
const frame = Cesium.Transforms.eastNorthUpToFixedFrame(
center || camera.positionWC,
scene.globe.ellipsoid,
);

camera.lookAtTransform(frame);

if (center) {
camera.rotateLeft(x);
camera.rotateUp(y);
} else {
camera.look(Cesium.Cartesian3.UNIT_Z, x);
camera.look(camera.right, y);
}
camera.lookAtTransform(oldTransform);
},
rotateRight: radian => {
const viewer = cesium.current?.cesiumElement;
if (!viewer || viewer.isDestroyed()) return;
const scene = viewer.scene;
const camera = scene.camera;
const oldTransform = Cesium.Matrix4.clone(camera.transform);
const frame = Cesium.Transforms.eastNorthUpToFixedFrame(
camera.positionWC,
scene.globe.ellipsoid,
);
camera.lookAtTransform(frame);
camera.rotateRight(radian - -camera.heading);
camera.lookAtTransform(oldTransform);
},
changeSceneMode: (sceneMode, duration = 2) => {
const viewer = cesium.current?.cesiumElement;
Expand Down
6 changes: 4 additions & 2 deletions src/components/molecules/Visualizer/Engine/ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ export type EngineRef = {
getLocationFromScreenXY: (x: number, y: number) => LatLngHeight | undefined;
flyTo: (destination: FlyToDestination, options?: CameraOptions) => void;
lookAt: (destination: LookAtDestination, options?: CameraOptions) => void;
zoomIn: (amount: number) => void;
zoomOut: (amount: number) => void;
zoomIn: (amount: number, options?: CameraOptions) => void;
zoomOut: (amount: number, options?: CameraOptions) => void;
orbit: (radian: number) => void;
rotateRight: (radian: number) => void;
changeSceneMode: (sceneMode: SceneMode | undefined, duration?: number) => void;
getClock: () => Clock | undefined;
captureScreen: (type?: string, encoderOptions?: number) => string | undefined;
Expand Down
Loading

0 comments on commit 3befd4c

Please sign in to comment.