Skip to content
This repository has been archived by the owner on Apr 25, 2023. It is now read-only.

feat: extend plugin API with camera control #311

Merged
merged 28 commits into from
Sep 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
193fbec
feat: add support of set screen space camera ctrl
airslice Aug 24, 2022
004c62c
Merge branch 'main' into feat/plugin-api-support-disable-camera-contr…
airslice Aug 26, 2022
67cdedf
Merge branch 'main' into feat/plugin-api-support-disable-camera-contr…
airslice Aug 26, 2022
17594d6
Merge branch 'main' into feat/plugin-api-support-disable-camera-contr…
airslice Aug 29, 2022
3146026
Merge branch 'main' into feat/plugin-api-support-disable-camera-contr…
airslice Aug 29, 2022
37f04cc
feat: extend camera API
airslice Aug 30, 2022
4c02156
feat: expose setView API
airslice Sep 2, 2022
e61bb40
Merge branch 'main' into feat/plugin-api-extend-camera
airslice Sep 2, 2022
c759fb2
feat: expose setView API
airslice Sep 2, 2022
d2442b0
Merge branch 'main' into feat/plugin-api-extend-camera
airslice Sep 2, 2022
0daa096
Merge branch 'main' into feat/plugin-api-extend-camera
airslice Sep 7, 2022
b56c655
refactor: clean code
airslice Sep 7, 2022
12ef9b5
feat: add lookHorizontal lookVertical
airslice Sep 7, 2022
f621bf4
feat: lookVertical add max angle
airslice Sep 9, 2022
f6a2db6
feat: remove lookUp and lookRight
airslice Sep 9, 2022
92e0509
feat: remove setView
airslice Sep 9, 2022
9518cc3
refactor: update api name and type ref
airslice Sep 9, 2022
168289e
feat: add moveOverTerrain & update lookXXX
airslice Sep 9, 2022
8b080be
feat: add flyToGround
airslice Sep 9, 2022
1805ab6
refactor: move functions into common
airslice Sep 9, 2022
9474d8f
Merge branch 'main' into feat/plugin-api-extend-camera
airslice Sep 9, 2022
d8a1438
refactor: clear function params
airslice Sep 12, 2022
82a6d35
test: add unit test for move and look
airslice Sep 12, 2022
2c262ce
Merge branch 'main' into feat/plugin-api-extend-camera
airslice Sep 12, 2022
19c5ef3
refactor: set type as any
airslice Sep 14, 2022
bd1e7fc
feat: combine enableScreenSpaceCameraController
airslice Sep 14, 2022
d0b5033
Merge branch 'main' into feat/plugin-api-extend-camera
airslice Sep 14, 2022
3bd66c7
refactor: also set type as any on other tests
airslice Sep 14, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
150 changes: 149 additions & 1 deletion src/components/molecules/Visualizer/Engine/Cesium/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,14 @@ import {
Clock as CesiumClock,
JulianDate,
ClockStep,
Ellipsoid,
Quaternion,
Matrix3,
Cartographic,
EllipsoidTerrainProvider,
sampleTerrainMostDetailed,
} from "cesium";
import { useCallback } from "react";
import { useCallback, MutableRefObject } from "react";

import { useCanvas, useImage } from "@reearth/util/image";
import { tweenInterval } from "@reearth/util/raf";
Expand Down Expand Up @@ -439,3 +445,145 @@ export function attachTag(entity: Entity | undefined, tag: string, value: any) {
entity.properties?.addProperty(tag, value);
}
}

export function lookHorizontal(scene: Scene, amount: number) {
const camera = scene.camera;
const ellipsoid = scene.globe.ellipsoid;
const surfaceNormal = ellipsoid.geodeticSurfaceNormal(camera.position, new Cartesian3());
camera.look(surfaceNormal, amount);
}

export function lookVertical(scene: Scene, amount: number) {
const camera = scene.camera;
const ellipsoid = scene.globe.ellipsoid;
const lookAxis = projectVectorToSurface(camera.right, camera.position, ellipsoid);
const surfaceNormal = ellipsoid.geodeticSurfaceNormal(camera.position, new Cartesian3());
const currentAngle = CesiumMath.toDegrees(Cartesian3.angleBetween(surfaceNormal, camera.up));
const upAfterLook = rotateVectorAboutAxis(camera.up, lookAxis, amount);
const angleAfterLook = CesiumMath.toDegrees(Cartesian3.angleBetween(surfaceNormal, upAfterLook));
const friction = angleAfterLook < currentAngle ? 1 : (90 - currentAngle) / 90;
camera.look(lookAxis, amount * friction);
}

export function moveForward(scene: Scene, amount: number) {
const direction = projectVectorToSurface(
scene.camera.direction,
scene.camera.position,
scene.globe.ellipsoid,
);
scene.camera.move(direction, amount);
}

export function moveBackward(scene: Scene, amount: number) {
const direction = projectVectorToSurface(
scene.camera.direction,
scene.camera.position,
scene.globe.ellipsoid,
);
scene.camera.move(direction, -amount);
}

export function moveUp(scene: Scene, amount: number) {
const surfaceNormal = scene.globe.ellipsoid.geodeticSurfaceNormal(
scene.camera.position,
new Cartesian3(),
);
scene.camera.move(surfaceNormal, amount);
}

export function moveDown(scene: Scene, amount: number) {
const surfaceNormal = scene.globe.ellipsoid.geodeticSurfaceNormal(
scene.camera.position,
new Cartesian3(),
);
scene.camera.move(surfaceNormal, -amount);
}

export function moveLeft(scene: Scene, amount: number) {
const direction = projectVectorToSurface(
scene.camera.right,
scene.camera.position,
scene.globe.ellipsoid,
);
scene.camera.move(direction, -amount);
}

export function moveRight(scene: Scene, amount: number) {
const direction = projectVectorToSurface(
scene.camera.right,
scene.camera.position,
scene.globe.ellipsoid,
);
scene.camera.move(direction, amount);
}

export async function moveOverTerrain(viewer: Viewer, offset = 0) {
const camera = viewer.scene.camera;
const height = await sampleTerrainHeight(viewer.scene, camera.position);
if (height && height !== 0) {
const innerCamera = getCamera(viewer);
if (innerCamera && innerCamera?.height < height + offset) {
camera.moveUp(height + offset - innerCamera.height);
}
}
}

export async function flyToGround(
viewer: Viewer,
cancelCameraFlight: MutableRefObject<(() => void) | undefined>,
camera?: {
lat?: number;
lng?: number;
height?: number;
heading?: number;
pitch?: number;
roll?: number;
fov?: number;
},
options?: {
duration?: number;
easing?: (time: number) => number;
},
offset = 0,
) {
const height = await sampleTerrainHeight(viewer.scene, viewer.scene.camera.position);
const tarHeight = height ? height + offset : offset;
const groundCamera = { ...camera, height: tarHeight };
cancelCameraFlight.current?.();
cancelCameraFlight.current = flyTo(
viewer.scene?.camera,
{ ...getCamera(viewer), ...groundCamera },
options,
);
}

function projectVectorToSurface(vector: Cartesian3, position: Cartesian3, ellipsoid: Ellipsoid) {
const surfaceNormal = ellipsoid.geodeticSurfaceNormal(position, new Cartesian3());
const magnitudeOfProjectionOnSurfaceNormal = Cartesian3.dot(vector, surfaceNormal);
const projectionOnSurfaceNormal = Cartesian3.multiplyByScalar(
surfaceNormal,
magnitudeOfProjectionOnSurfaceNormal,
new Cartesian3(),
);
return Cartesian3.subtract(vector, projectionOnSurfaceNormal, new Cartesian3());
}

function rotateVectorAboutAxis(vector: Cartesian3, rotateAxis: Cartesian3, rotateAmount: number) {
const quaternion = Quaternion.fromAxisAngle(rotateAxis, -rotateAmount, new Quaternion());
const rotation = Matrix3.fromQuaternion(quaternion, new Matrix3());
const rotatedVector = Matrix3.multiplyByVector(rotation, vector, vector.clone());
return rotatedVector;
}

async function sampleTerrainHeight(
scene: Scene,
position: Cartesian3,
): Promise<number | undefined> {
const terrainProvider = scene.terrainProvider;
if (terrainProvider instanceof EllipsoidTerrainProvider) return 0;

const [sample] = await sampleTerrainMostDetailed(terrainProvider, [
Cartographic.fromCartesian(position, scene.globe.ellipsoid, new Cartographic()),
]);
return sample.height;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { renderHook } from "@testing-library/react";
import { ClockStep, JulianDate, Viewer as CesiumViewer } from "cesium";
import {
ClockStep,
JulianDate,
Viewer as CesiumViewer,
Cartesian3,
Globe,
Ellipsoid,
} from "cesium";
import { useRef } from "react";
import type { CesiumComponentRef } from "resium";
import { vi, expect, test } from "vitest";
Expand Down Expand Up @@ -134,15 +141,13 @@ test("requestRender", () => {
const { result } = renderHook(() => {
const cesium = useRef<CesiumComponentRef<CesiumViewer>>({
cesiumElement: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
scene: {
requestRender: mockRequestRender,
},
isDestroyed: () => {
return false;
},
},
} as any,
});
const engineRef = useRef<EngineRef>(null);
useEngineRef(engineRef, cesium);
Expand All @@ -158,11 +163,7 @@ test("zoom", () => {
const { result } = renderHook(() => {
const cesium = useRef<CesiumComponentRef<CesiumViewer>>({
cesiumElement: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
scene: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
camera: {
zoomIn: mockZoomIn,
zoomOut: mockZoomOut,
Expand All @@ -171,7 +172,7 @@ test("zoom", () => {
isDestroyed: () => {
return false;
},
},
} as any,
});
const engineRef = useRef<EngineRef>(null);
useEngineRef(engineRef, cesium);
Expand Down Expand Up @@ -199,8 +200,6 @@ test("getClock", () => {
const { result } = renderHook(() => {
const cesium = useRef<CesiumComponentRef<CesiumViewer>>({
cesiumElement: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
clock: {
startTime,
stopTime,
Expand All @@ -209,8 +208,6 @@ test("getClock", () => {
shouldAnimate: false,
multiplier: 1,
clockStep: ClockStep.SYSTEM_CLOCK,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// TODO: should test cesium event
onTick: {
addEventListener: mockAddEventHandler,
Expand All @@ -220,7 +217,7 @@ test("getClock", () => {
isDestroyed: () => {
return false;
},
},
} as any,
});
const engineRef = useRef<EngineRef>(null);
useEngineRef(engineRef, cesium);
Expand Down Expand Up @@ -284,12 +281,10 @@ test("captureScreen", () => {
cesiumElement: {
render: mockViewerRender,
isDestroyed: mockViewerIsDestroyed,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
canvas: {
toDataURL: mockCanvasToDataURL,
},
},
} as any,
});
const engineRef = useRef<EngineRef>(null);
useEngineRef(engineRef, cesium);
Expand All @@ -303,3 +298,83 @@ test("captureScreen", () => {
result.current.current?.captureScreen("image/jpeg", 0.8);
expect(mockCanvasToDataURL).toHaveBeenCalledWith("image/jpeg", 0.8);
});

test("move", () => {
const mockMove = vi.fn(e => e);
const { result } = renderHook(() => {
const cesium = useRef<CesiumComponentRef<CesiumViewer>>({
cesiumElement: {
isDestroyed: () => false,
scene: {
camera: {
position: new Cartesian3(0, 0, 1),
direction: Cartesian3.clone(Cartesian3.UNIT_X),
up: Cartesian3.clone(Cartesian3.UNIT_Z),
right: Cartesian3.clone(Cartesian3.UNIT_Y),
move: mockMove,
},
globe: new Globe(Ellipsoid.UNIT_SPHERE),
},
} as any,
});
const engineRef = useRef<EngineRef>(null);
useEngineRef(engineRef, cesium);
return engineRef;
});

result.current.current?.moveForward(100);
expect(mockMove).toHaveBeenCalledTimes(1);
expect(mockMove).toHaveBeenLastCalledWith(new Cartesian3(1, 0, 0), 100);

result.current.current?.moveBackward(100);
expect(mockMove).toHaveBeenCalledTimes(2);
expect(mockMove).toHaveBeenLastCalledWith(new Cartesian3(1, 0, 0), -100);

result.current.current?.moveUp(100);
expect(mockMove).toHaveBeenCalledTimes(3);
expect(mockMove).toHaveBeenLastCalledWith(new Cartesian3(0, 0, 1), 100);

result.current.current?.moveDown(100);
expect(mockMove).toHaveBeenCalledTimes(4);
expect(mockMove).toHaveBeenLastCalledWith(new Cartesian3(0, 0, 1), -100);

result.current.current?.moveRight(100);
expect(mockMove).toHaveBeenCalledTimes(5);
expect(mockMove).toHaveBeenLastCalledWith(new Cartesian3(0, 1, 0), 100);

result.current.current?.moveLeft(100);
expect(mockMove).toHaveBeenCalledTimes(6);
expect(mockMove).toHaveBeenLastCalledWith(new Cartesian3(0, 1, 0), -100);
});

test("look", () => {
const mockLook = vi.fn(e => e);
const { result } = renderHook(() => {
const cesium = useRef<CesiumComponentRef<CesiumViewer>>({
cesiumElement: {
isDestroyed: () => false,
scene: {
camera: {
position: new Cartesian3(0, 0, 1),
direction: Cartesian3.clone(Cartesian3.UNIT_X),
up: Cartesian3.clone(Cartesian3.UNIT_Z),
right: Cartesian3.clone(Cartesian3.UNIT_Y),
look: mockLook,
},
globe: new Globe(Ellipsoid.UNIT_SPHERE),
},
} as any,
});
const engineRef = useRef<EngineRef>(null);
useEngineRef(engineRef, cesium);
return engineRef;
});

result.current.current?.lookHorizontal(90);
expect(mockLook).toHaveBeenCalledTimes(1);
expect(mockLook).toHaveBeenLastCalledWith(new Cartesian3(0, 0, 1), 90);

result.current.current?.lookVertical(90);
expect(mockLook).toHaveBeenCalledTimes(2);
expect(mockLook).toHaveBeenLastCalledWith(new Cartesian3(0, 1, 0), 90);
});