From a08a550c3afbe146fd8e17f272064c31cd09edec Mon Sep 17 00:00:00 2001 From: glughi Date: Fri, 15 Nov 2024 10:01:41 +0100 Subject: [PATCH 1/3] Working on map navigation with keyboard --- .../MapNavigation/registerMapNavigations.tsx | 17 + .../Tools/KeyboardMode/KeyboardMode.tsx | 43 ++ .../Tools/KeyboardMode/MovementControls.tsx | 99 ++++ .../Tools/KeyboardMode/MovementsController.ts | 480 ++++++++++++++++++ 4 files changed, 639 insertions(+) create mode 100644 lib/ReactViews/Tools/KeyboardMode/KeyboardMode.tsx create mode 100644 lib/ReactViews/Tools/KeyboardMode/MovementControls.tsx create mode 100644 lib/ReactViews/Tools/KeyboardMode/MovementsController.ts diff --git a/lib/ReactViews/Map/MapNavigation/registerMapNavigations.tsx b/lib/ReactViews/Map/MapNavigation/registerMapNavigations.tsx index 905dcf60682..78438651cf9 100644 --- a/lib/ReactViews/Map/MapNavigation/registerMapNavigations.tsx +++ b/lib/ReactViews/Map/MapNavigation/registerMapNavigations.tsx @@ -30,6 +30,7 @@ import { ZOOM_CONTROL_ID } from "./Items"; import { TogglePickInfoController } from "./Items/TogglePickInfoTool"; +import KeyboardMode, { KEYBOARD_MODE_ID } from "../../Tools/KeyboardMode/KeyboardMode"; export const CLOSE_TOOL_ID = "close-tool"; @@ -248,6 +249,22 @@ export const registerMapNavigations = (viewState: ViewState) => { noExpand: true }); + const keyboardModeToolController = new ToolButtonController({ + toolName: KEYBOARD_MODE_ID, + viewState: viewState, + getToolComponent: () => KeyboardMode as any, + icon: GLYPHS.tour + }); + mapNavigationModel.addItem({ + id: KEYBOARD_MODE_ID, + name: "translate#pedestrianMode.toolButtonTitle", + title: "translate#pedestrianMode.toolButtonTitle", + location: "TOP", + screenSize: "medium", + controller: keyboardModeToolController, + order: 9 + }); + const feedbackController = new FeedbackButtonController(viewState); mapNavigationModel.addItem({ id: FEEDBACK_TOOL_ID, diff --git a/lib/ReactViews/Tools/KeyboardMode/KeyboardMode.tsx b/lib/ReactViews/Tools/KeyboardMode/KeyboardMode.tsx new file mode 100644 index 00000000000..cb59cba008c --- /dev/null +++ b/lib/ReactViews/Tools/KeyboardMode/KeyboardMode.tsx @@ -0,0 +1,43 @@ +import { reaction } from "mobx"; +import { observer } from "mobx-react"; +import React, { useEffect, useState } from "react"; +import styled from "styled-components"; +import Cesium from "../../../Models/Cesium"; +import ViewState from "../../../ReactViewModels/ViewState"; +import PositionRightOfWorkbench from "../../Workbench/PositionRightOfWorkbench"; +import MovementControls from "./MovementControls"; + + +type PedestrianModeProps = { + viewState: ViewState; +}; +export const KEYBOARD_MODE_ID = "keyboard-mode"; + +const KeyboardMode: React.FC = observer((props) => { + const { viewState } = props; + + const cesium = viewState.terria.currentViewer; + + if (!(cesium instanceof Cesium)) { + viewState.closeTool(); + return null; + } + + return ( + + + + ); +}); + +const ControlsContainer = styled(PositionRightOfWorkbench)` + width: 140px; + top: unset; + left: 0; + bottom: 300px; +`; + + +export default KeyboardMode; diff --git a/lib/ReactViews/Tools/KeyboardMode/MovementControls.tsx b/lib/ReactViews/Tools/KeyboardMode/MovementControls.tsx new file mode 100644 index 00000000000..1b27800ad2f --- /dev/null +++ b/lib/ReactViews/Tools/KeyboardMode/MovementControls.tsx @@ -0,0 +1,99 @@ +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import styled from "styled-components"; +import Cesium from "../../../Models/Cesium"; +import Box from "../../../Styled/Box"; +import Button from "../../../Styled/Button"; +import Spacing from "../../../Styled/Spacing"; +import Text from "../../../Styled/Text"; +import Icon, { StyledIcon } from "../../../Styled/Icon"; +import MovementsController from "./MovementsController"; + + +const mouseControlsImage = require("../../../../wwwroot/images/mouse-control.svg"); +const wasdControlsImage = require("../../../../wwwroot/images/wasd.svg"); +const heightControlsImage = require("../../../../wwwroot/images/height-controls.svg"); + +type MovementControlsProps = { + cesium: Cesium; +}; + +const MovementControls: React.FC = (props) => { + const [isMaximized, setIsMaximized] = useState(true); + const [t] = useTranslation(); + + const toggleMaximized = () => setIsMaximized(!isMaximized); + + useEffect(() => { + const movementsController = new MovementsController( + props.cesium + ); + const detach = movementsController.activate(); + return detach; + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + }, [props.cesium]); + + return ( + + + <Text medium>{t("pedestrianMode.controls.title")}</Text> + <MinimizeMaximizeButton + onClick={toggleMaximized} + maximized={isMaximized} + /> + + {isMaximized && ( + + Mouse controls + Direction controls + + Height controls + + )} + + ); +}; + +const backgroundColor = "#ffffff"; + +const Container = styled.div` + background-color: ${backgroundColor}; + box-shadow: 0 4px 8px 4px rgb(0 0 0 / 5%); + border-radius: 3px; +`; + +const Title = styled(Box).attrs({ + medium: true +})` + justify-content: space-between; + align-items: center; + padding: 0 0.5em; + border-bottom: 1px solid #c0c0c0; +`; + +const MinimizeMaximizeButton = styled(Button).attrs(({ maximized }) => ({ + renderIcon: () => ( + + ) +})) <{ maximized: boolean }>` + padding: 0; + margin: 0; + border: 0; + background-color: ${backgroundColor}; +`; + +const ButtonIcon = styled(StyledIcon)` + height: 20px; +`; + +const Body = styled(Box).attrs({ column: true, centered: true })` + align-items: center; + margin-top: 1em; + & img { + padding-bottom: 1em; + } +`; + +export default MovementControls; diff --git a/lib/ReactViews/Tools/KeyboardMode/MovementsController.ts b/lib/ReactViews/Tools/KeyboardMode/MovementsController.ts new file mode 100644 index 00000000000..8cc86f48c0a --- /dev/null +++ b/lib/ReactViews/Tools/KeyboardMode/MovementsController.ts @@ -0,0 +1,480 @@ +import { action, makeObservable } from "mobx"; +import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2"; +import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; +import Cartographic from "terriajs-cesium/Source/Core/Cartographic"; +import Ellipsoid from "terriajs-cesium/Source/Core/Ellipsoid"; +import EllipsoidTerrainProvider from "terriajs-cesium/Source/Core/EllipsoidTerrainProvider"; +import KeyboardEventModifier from "terriajs-cesium/Source/Core/KeyboardEventModifier"; +import CesiumMath from "terriajs-cesium/Source/Core/Math"; +import Matrix3 from "terriajs-cesium/Source/Core/Matrix3"; +import Quaternion from "terriajs-cesium/Source/Core/Quaternion"; +import sampleTerrainMostDetailed from "terriajs-cesium/Source/Core/sampleTerrainMostDetailed"; +import ScreenSpaceEventHandler from "terriajs-cesium/Source/Core/ScreenSpaceEventHandler"; +import ScreenSpaceEventType from "terriajs-cesium/Source/Core/ScreenSpaceEventType"; +import Scene from "terriajs-cesium/Source/Scene/Scene"; +import Cesium from "../../../Models/Cesium"; + +type Movement = + | "forward" + | "backward" + | "left" + | "right" + | "up" + | "down" + | "look"; + +export type Mode = "walk" | "fly"; + +const KeyMap: Record = { + KeyW: "forward", + KeyA: "left", + KeyS: "backward", + KeyD: "right", + Space: "up", + ShiftLeft: "down", + ShiftRight: "down" +}; + +export default class MovementsController { + // Current active movements + activeMovements: Set = new Set(); + + // True if we are currently updating surface height estimate + isUpdatingSurfaceHeightEstimate = false; + + // True if we are currently animating surface height change + isAnimatingSurfaceHeightChange = false; + + // The position of the mouse when a mouse action is started + private startMousePosition?: Cartesian2; + + // The latest position of the mouse while the action is active + private currentMousePosition?: Cartesian2; + + constructor( + readonly cesium: Cesium, + ) { + makeObservable(this); + } + + get scene() { + return this.cesium.scene; + } + + get camera() { + return this.scene.camera; + } + + /** + * moveAmount decides the motion speed. + */ + get moveAmount() { + //const baseAmount = 0.2; + const cameraHeight = this.camera.positionCartographic.height; + const moveRate = cameraHeight / 100.0; + return moveRate; + } + + /** + * Moves the camera forward and parallel to the surface by moveAmount + */ + moveForward() { + const direction = projectVectorToSurface( + this.camera.direction, + this.camera.position, + this.scene.globe.ellipsoid + ); + this.camera.move(direction, this.moveAmount); + + + console.log("forwww") + console.log(this.moveAmount) + console.log(direction) + + this.cesium.notifyRepaintRequired(); + } + + /** + * Moves the camera backward and parallel to the surface by moveAmount + */ + moveBackward() { + const direction = projectVectorToSurface( + this.camera.direction, + this.camera.position, + this.scene.globe.ellipsoid + ); + this.camera.move(direction, -this.moveAmount); + } + + /** + * Moves the camera left and parallel to the surface by moveAmount/4 + */ + moveLeft() { + const direction = projectVectorToSurface( + this.camera.right, + this.camera.position, + this.scene.globe.ellipsoid + ); + this.camera.move(direction, -this.moveAmount / 4); + } + + /** + * Moves the camera right and parallel to the surface by moveAmount/4 + */ + moveRight() { + const direction = projectVectorToSurface( + this.camera.right, + this.camera.position, + this.scene.globe.ellipsoid + ); + this.camera.move(direction, this.moveAmount / 4); + } + + /** + * Moves the camera up and perpendicular to the surface by moveAmount + */ + moveUp() { + const surfaceNormal = this.scene.globe.ellipsoid.geodeticSurfaceNormal( + this.camera.position, + new Cartesian3() + ); + this.camera.move(surfaceNormal, this.moveAmount); + } + + /** + * Moves the camera up and perpendicular to the surface by moveAmount + */ + moveDown() { + const surfaceNormal = this.scene.globe.ellipsoid.geodeticSurfaceNormal( + this.camera.position, + new Cartesian3() + ); + this.camera.move(surfaceNormal, -this.moveAmount); + } + + look() { + if ( + this.startMousePosition === undefined || + this.currentMousePosition === undefined + ) + return; + + const startMousePosition = this.startMousePosition; + const currentMousePosition = this.currentMousePosition; + + const camera = this.scene.camera; + const canvas = this.scene.canvas; + const width = canvas.width; + const height = canvas.height; + const x = (currentMousePosition.x - startMousePosition.x) / width; + const y = (currentMousePosition.y - startMousePosition.y) / height; + const lookFactor = 0.1; + + const ellipsoid = this.scene.globe.ellipsoid; + const surfaceNormal = ellipsoid.geodeticSurfaceNormal( + camera.position, + new Cartesian3() + ); + + const surfaceTangent = projectVectorToSurface( + camera.right, + camera.position, + this.scene.globe.ellipsoid + ); + + // Look left/right about the surface normal + camera.look(surfaceNormal, x * lookFactor); + + // Look up/down about the surface tangent + this.lookVertical(surfaceTangent, surfaceNormal, y * lookFactor); + } + + /** + * Look up/down limiting the maximum look angle to {@maxLookangle} + * + */ + lookVertical( + lookAxis: Cartesian3, + surfaceNormal: Cartesian3, + lookAmount: number + ) { + const camera = this.camera; + + const friction = 1; + camera.look(lookAxis, lookAmount * friction); + } + + /** + * Perform a move step + */ + move(movement: Movement) { + switch (movement) { + case "forward": + return this.moveForward(); + case "backward": + return this.moveBackward(); + case "left": + return this.moveLeft(); + case "right": + return this.moveRight(); + case "up": + return this.moveUp(); + case "down": + return this.moveDown(); + case "look": + return this.look(); + } + } + + animate() { + if (this.activeMovements.size > 0) { + console.log("aaaaaaaaaaa"); + [...this.activeMovements].forEach((movement) => this.move(movement)); + } + } + + /** + * Map keyboard events to movements + */ + setupKeyMap(): () => void { + const onKeyDown = (ev: KeyboardEvent) => { + if ( + // do not match if any modifiers are pressed so that we do not hijack window shortcuts. + ev.ctrlKey === false && + ev.altKey === false && + KeyMap[ev.code] !== undefined + ) + this.activeMovements.add(KeyMap[ev.code]); + }; + + const onKeyUp = (ev: KeyboardEvent) => { + if (KeyMap[ev.code] !== undefined) + this.activeMovements.delete(KeyMap[ev.code]); + }; + + document.addEventListener("keydown", excludeInputEvents(onKeyDown), true); + document.addEventListener("keyup", excludeInputEvents(onKeyUp), true); + + const keyMapDestroyer = () => { + document.removeEventListener("keydown", onKeyDown); + document.removeEventListener("keyup", onKeyUp); + }; + + return keyMapDestroyer; + } + + /** + * Map mouse events to movements + */ + setupMouseMap(): () => void { + const eventHandler = new ScreenSpaceEventHandler(this.scene.canvas); + + const startLook = (click: { position: Cartesian2 }) => { + this.currentMousePosition = this.startMousePosition = + click.position.clone(); + this.activeMovements.add("look"); + }; + + const look = (movement: { endPosition: Cartesian2 }) => { + this.currentMousePosition = movement.endPosition.clone(); + }; + + const stopLook = () => { + this.activeMovements.delete("look"); + this.currentMousePosition = this.startMousePosition = undefined; + }; + + // User might try to turn while moving down (by pressing SHIFT) + // so trigger look event even when SHIFT is pressed. + eventHandler.setInputAction(startLook, ScreenSpaceEventType.LEFT_DOWN); + eventHandler.setInputAction( + startLook, + ScreenSpaceEventType.LEFT_DOWN, + KeyboardEventModifier.SHIFT + ); + + eventHandler.setInputAction(look, ScreenSpaceEventType.MOUSE_MOVE); + eventHandler.setInputAction( + look, + ScreenSpaceEventType.MOUSE_MOVE, + KeyboardEventModifier.SHIFT + ); + + eventHandler.setInputAction(stopLook, ScreenSpaceEventType.LEFT_UP); + eventHandler.setInputAction( + stopLook, + ScreenSpaceEventType.LEFT_UP, + KeyboardEventModifier.SHIFT + ); + const mouseMapDestroyer = () => eventHandler.destroy(); + return mouseMapDestroyer; + } + + /** + * Animate on each clock tick + */ + startAnimating() { + const stopAnimating = + this.cesium.cesiumWidget.clock.onTick.addEventListener( + this.animate.bind(this) + ); + return stopAnimating; + } + + /** + * Activates MovementsController + * + * 1. Disables default map interactions. + * 2. Sets up keyboard, mouse & animation event handlers. + * + * @returns A function to de-activate the movements controller + */ + @action + activate(): () => void { + // Disable other map controls + this.scene.screenSpaceCameraController.enableTranslate = false; + this.scene.screenSpaceCameraController.enableRotate = false; + this.scene.screenSpaceCameraController.enableLook = false; + this.scene.screenSpaceCameraController.enableTilt = false; + this.scene.screenSpaceCameraController.enableZoom = false; + this.cesium.isFeaturePickingPaused = true; + + const destroyKeyMap = this.setupKeyMap(); + const destroyMouseMap = this.setupMouseMap(); + const stopAnimating = this.startAnimating(); + + const deactivate = action(() => { + destroyKeyMap(); + destroyMouseMap(); + stopAnimating(); + + const screenSpaceCameraController = + this.scene.screenSpaceCameraController; + // screenSpaceCameraController will be undefined if the cesium map is already destroyed + if (screenSpaceCameraController !== undefined) { + screenSpaceCameraController.enableTranslate = true; + screenSpaceCameraController.enableRotate = true; + screenSpaceCameraController.enableLook = true; + screenSpaceCameraController.enableTilt = true; + screenSpaceCameraController.enableZoom = true; + } + this.cesium.isFeaturePickingPaused = false; + }); + + return deactivate; + } +} + +const sampleScratch = new Cartographic(); + +/** + * Sample the terrain height at the given position + */ +async function sampleTerrainHeight( + scene: Scene, + position: Cartesian3 +): Promise { + const terrainProvider = scene.terrainProvider; + if (terrainProvider instanceof EllipsoidTerrainProvider) return 0; + + const [sample] = await sampleTerrainMostDetailed(terrainProvider, [ + Cartographic.fromCartesian(position, scene.globe.ellipsoid, sampleScratch) + ]); + return sample.height; +} + +/** + * Sample the scene height at the given position + * + * Scene height is the maximum height of a tileset feature or any other entity + * at the given position. + */ +function sampleSceneHeight( + scene: Scene, + position: Cartesian3 +): number | undefined { + if (scene.sampleHeightSupported === false) return; + return scene.sampleHeight( + Cartographic.fromCartesian(position, undefined, sampleScratch) + ); +} + +/** + * Projects the {@vector} to the surface plane containing {@position} + * + * @param vector The input vector to project + * @param position The position used to determine the surface plane + * @param ellipsoid The ellipsoid used to compute the surface plane + * @returns The projection of {@vector} on the surface plane at the given {@position} + */ +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() + ); + const projectionOnSurface = Cartesian3.subtract( + vector, + projectionOnSurfaceNormal, + new Cartesian3() + ); + return projectionOnSurface; +} + +const rotateScratchQuaternion = new Quaternion(); +const rotateScratchMatrix = new Matrix3(); + +/** + * Rotates a vector about rotateAxis by rotateAmount + */ +function rotateVectorAboutAxis( + vector: Cartesian3, + rotateAxis: Cartesian3, + rotateAmount: number +) { + const quaternion = Quaternion.fromAxisAngle( + rotateAxis, + -rotateAmount, + rotateScratchQuaternion + ); + const rotation = Matrix3.fromQuaternion(quaternion, rotateScratchMatrix); + const rotatedVector = Matrix3.multiplyByVector( + rotation, + vector, + vector.clone() + ); + return rotatedVector; +} + +// A regex matching input tag names +const inputNodeRe = /input|textarea|select/i; + +function excludeInputEvents( + handler: (ev: KeyboardEvent) => void +): (ev: KeyboardEvent) => void { + return (ev) => { + const target = ev.target; + if (target !== null) { + const nodeName = (target as any).nodeName; + const isContentEditable = (target as any).getAttribute?.( + "contenteditable" + ); + if (isContentEditable || inputNodeRe.test(nodeName)) { + return; + } + } + handler(ev); + }; +} From 473d70c8ea4f93841ee77b26885de93b3391d6c1 Mon Sep 17 00:00:00 2001 From: glughi Date: Wed, 20 Nov 2024 11:42:24 +0100 Subject: [PATCH 2/3] Done map navigation with keyboard --- .../MapNavigation/registerMapNavigations.tsx | 6 +- .../Tools/KeyboardMode/KeyboardMode.tsx | 7 +- .../Tools/KeyboardMode/MovementControls.tsx | 11 +- .../Tools/KeyboardMode/MovementsController.ts | 333 ++---------------- lib/Styled/Icon.tsx | 1 + wwwroot/images/icons/keyboard.svg | 60 ++++ wwwroot/images/keyboard_controls.svg | 262 ++++++++++++++ wwwroot/languages/en/translation.json | 4 + wwwroot/languages/it/translation.json | 4 + 9 files changed, 373 insertions(+), 315 deletions(-) create mode 100644 wwwroot/images/icons/keyboard.svg create mode 100644 wwwroot/images/keyboard_controls.svg diff --git a/lib/ReactViews/Map/MapNavigation/registerMapNavigations.tsx b/lib/ReactViews/Map/MapNavigation/registerMapNavigations.tsx index 78438651cf9..8d8ec7667ae 100644 --- a/lib/ReactViews/Map/MapNavigation/registerMapNavigations.tsx +++ b/lib/ReactViews/Map/MapNavigation/registerMapNavigations.tsx @@ -253,12 +253,12 @@ export const registerMapNavigations = (viewState: ViewState) => { toolName: KEYBOARD_MODE_ID, viewState: viewState, getToolComponent: () => KeyboardMode as any, - icon: GLYPHS.tour + icon: GLYPHS.keyboard }); mapNavigationModel.addItem({ id: KEYBOARD_MODE_ID, - name: "translate#pedestrianMode.toolButtonTitle", - title: "translate#pedestrianMode.toolButtonTitle", + name: "translate#keyboardControls.toolButtonTitle", + title: "translate#keyboardControls.toolButtonTitle", location: "TOP", screenSize: "medium", controller: keyboardModeToolController, diff --git a/lib/ReactViews/Tools/KeyboardMode/KeyboardMode.tsx b/lib/ReactViews/Tools/KeyboardMode/KeyboardMode.tsx index cb59cba008c..e3ed212f081 100644 --- a/lib/ReactViews/Tools/KeyboardMode/KeyboardMode.tsx +++ b/lib/ReactViews/Tools/KeyboardMode/KeyboardMode.tsx @@ -1,6 +1,5 @@ -import { reaction } from "mobx"; import { observer } from "mobx-react"; -import React, { useEffect, useState } from "react"; +import React from "react"; import styled from "styled-components"; import Cesium from "../../../Models/Cesium"; import ViewState from "../../../ReactViewModels/ViewState"; @@ -8,12 +7,12 @@ import PositionRightOfWorkbench from "../../Workbench/PositionRightOfWorkbench"; import MovementControls from "./MovementControls"; -type PedestrianModeProps = { +type KeyboardModeProps = { viewState: ViewState; }; export const KEYBOARD_MODE_ID = "keyboard-mode"; -const KeyboardMode: React.FC = observer((props) => { +const KeyboardMode: React.FC = observer((props) => { const { viewState } = props; const cesium = viewState.terria.currentViewer; diff --git a/lib/ReactViews/Tools/KeyboardMode/MovementControls.tsx b/lib/ReactViews/Tools/KeyboardMode/MovementControls.tsx index 1b27800ad2f..81ab5c43eba 100644 --- a/lib/ReactViews/Tools/KeyboardMode/MovementControls.tsx +++ b/lib/ReactViews/Tools/KeyboardMode/MovementControls.tsx @@ -4,15 +4,11 @@ import styled from "styled-components"; import Cesium from "../../../Models/Cesium"; import Box from "../../../Styled/Box"; import Button from "../../../Styled/Button"; -import Spacing from "../../../Styled/Spacing"; import Text from "../../../Styled/Text"; import Icon, { StyledIcon } from "../../../Styled/Icon"; import MovementsController from "./MovementsController"; - -const mouseControlsImage = require("../../../../wwwroot/images/mouse-control.svg"); -const wasdControlsImage = require("../../../../wwwroot/images/wasd.svg"); -const heightControlsImage = require("../../../../wwwroot/images/height-controls.svg"); +const wasdControlsImage = require("../../../../wwwroot/images/keyboard_controls.svg"); type MovementControlsProps = { cesium: Cesium; @@ -36,7 +32,7 @@ const MovementControls: React.FC = (props) => { return ( - <Text medium>{t("pedestrianMode.controls.title")}</Text> + <Text medium>{t("keyboardControls.header")}</Text> <MinimizeMaximizeButton onClick={toggleMaximized} maximized={isMaximized} @@ -44,10 +40,7 @@ const MovementControls: React.FC<MovementControlsProps> = (props) => { {isMaximized && ( - Mouse controls Direction controls - - Height controls )} diff --git a/lib/ReactViews/Tools/KeyboardMode/MovementsController.ts b/lib/ReactViews/Tools/KeyboardMode/MovementsController.ts index 8cc86f48c0a..a1edfe8e7c4 100644 --- a/lib/ReactViews/Tools/KeyboardMode/MovementsController.ts +++ b/lib/ReactViews/Tools/KeyboardMode/MovementsController.ts @@ -1,17 +1,5 @@ import { action, makeObservable } from "mobx"; -import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2"; -import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; -import Cartographic from "terriajs-cesium/Source/Core/Cartographic"; -import Ellipsoid from "terriajs-cesium/Source/Core/Ellipsoid"; -import EllipsoidTerrainProvider from "terriajs-cesium/Source/Core/EllipsoidTerrainProvider"; -import KeyboardEventModifier from "terriajs-cesium/Source/Core/KeyboardEventModifier"; -import CesiumMath from "terriajs-cesium/Source/Core/Math"; -import Matrix3 from "terriajs-cesium/Source/Core/Matrix3"; -import Quaternion from "terriajs-cesium/Source/Core/Quaternion"; -import sampleTerrainMostDetailed from "terriajs-cesium/Source/Core/sampleTerrainMostDetailed"; import ScreenSpaceEventHandler from "terriajs-cesium/Source/Core/ScreenSpaceEventHandler"; -import ScreenSpaceEventType from "terriajs-cesium/Source/Core/ScreenSpaceEventType"; -import Scene from "terriajs-cesium/Source/Scene/Scene"; import Cesium from "../../../Models/Cesium"; type Movement = @@ -21,36 +9,28 @@ type Movement = | "right" | "up" | "down" - | "look"; - -export type Mode = "walk" | "fly"; + | "lookUp" + | "lookDown" + | "lookLeft" + | "lookRight"; const KeyMap: Record = { KeyW: "forward", KeyA: "left", KeyS: "backward", KeyD: "right", - Space: "up", - ShiftLeft: "down", - ShiftRight: "down" + KeyZ: "up", + KeyX: "down", + KeyQ: "lookLeft", + KeyE: "lookRight", + KeyR: "lookUp", + KeyF: "lookDown" }; export default class MovementsController { // Current active movements activeMovements: Set = new Set(); - // True if we are currently updating surface height estimate - isUpdatingSurfaceHeightEstimate = false; - - // True if we are currently animating surface height change - isAnimatingSurfaceHeightChange = false; - - // The position of the mouse when a mouse action is started - private startMousePosition?: Cartesian2; - - // The latest position of the mouse while the action is active - private currentMousePosition?: Cartesian2; - constructor( readonly cesium: Cesium, ) { @@ -69,166 +49,51 @@ export default class MovementsController { * moveAmount decides the motion speed. */ get moveAmount() { - //const baseAmount = 0.2; const cameraHeight = this.camera.positionCartographic.height; const moveRate = cameraHeight / 100.0; return moveRate; } - /** - * Moves the camera forward and parallel to the surface by moveAmount - */ - moveForward() { - const direction = projectVectorToSurface( - this.camera.direction, - this.camera.position, - this.scene.globe.ellipsoid - ); - this.camera.move(direction, this.moveAmount); - - - console.log("forwww") - console.log(this.moveAmount) - console.log(direction) - - this.cesium.notifyRepaintRequired(); - } - - /** - * Moves the camera backward and parallel to the surface by moveAmount - */ - moveBackward() { - const direction = projectVectorToSurface( - this.camera.direction, - this.camera.position, - this.scene.globe.ellipsoid - ); - this.camera.move(direction, -this.moveAmount); - } - - /** - * Moves the camera left and parallel to the surface by moveAmount/4 - */ - moveLeft() { - const direction = projectVectorToSurface( - this.camera.right, - this.camera.position, - this.scene.globe.ellipsoid - ); - this.camera.move(direction, -this.moveAmount / 4); - } - - /** - * Moves the camera right and parallel to the surface by moveAmount/4 - */ - moveRight() { - const direction = projectVectorToSurface( - this.camera.right, - this.camera.position, - this.scene.globe.ellipsoid - ); - this.camera.move(direction, this.moveAmount / 4); - } - - /** - * Moves the camera up and perpendicular to the surface by moveAmount - */ - moveUp() { - const surfaceNormal = this.scene.globe.ellipsoid.geodeticSurfaceNormal( - this.camera.position, - new Cartesian3() - ); - this.camera.move(surfaceNormal, this.moveAmount); - } - - /** - * Moves the camera up and perpendicular to the surface by moveAmount - */ - moveDown() { - const surfaceNormal = this.scene.globe.ellipsoid.geodeticSurfaceNormal( - this.camera.position, - new Cartesian3() - ); - this.camera.move(surfaceNormal, -this.moveAmount); - } - - look() { - if ( - this.startMousePosition === undefined || - this.currentMousePosition === undefined - ) - return; - - const startMousePosition = this.startMousePosition; - const currentMousePosition = this.currentMousePosition; - - const camera = this.scene.camera; - const canvas = this.scene.canvas; - const width = canvas.width; - const height = canvas.height; - const x = (currentMousePosition.x - startMousePosition.x) / width; - const y = (currentMousePosition.y - startMousePosition.y) / height; - const lookFactor = 0.1; - - const ellipsoid = this.scene.globe.ellipsoid; - const surfaceNormal = ellipsoid.geodeticSurfaceNormal( - camera.position, - new Cartesian3() - ); - - const surfaceTangent = projectVectorToSurface( - camera.right, - camera.position, - this.scene.globe.ellipsoid - ); - - // Look left/right about the surface normal - camera.look(surfaceNormal, x * lookFactor); - - // Look up/down about the surface tangent - this.lookVertical(surfaceTangent, surfaceNormal, y * lookFactor); - } - - /** - * Look up/down limiting the maximum look angle to {@maxLookangle} - * - */ - lookVertical( - lookAxis: Cartesian3, - surfaceNormal: Cartesian3, - lookAmount: number - ) { - const camera = this.camera; - - const friction = 1; - camera.look(lookAxis, lookAmount * friction); - } - /** * Perform a move step */ move(movement: Movement) { switch (movement) { case "forward": - return this.moveForward(); + this.camera.moveForward(this.moveAmount); + break; case "backward": - return this.moveBackward(); + this.camera.moveBackward(this.moveAmount); + break; case "left": - return this.moveLeft(); + this.camera.moveLeft(this.moveAmount); + break; case "right": - return this.moveRight(); + this.camera.moveRight(this.moveAmount); + break; case "up": - return this.moveUp(); + this.camera.moveUp(this.moveAmount); + break; case "down": - return this.moveDown(); - case "look": - return this.look(); + this.camera.moveDown(this.moveAmount); + break; + case "lookUp": + this.camera.lookUp(); + break; + case "lookDown": + this.camera.lookDown(); + break; + case "lookLeft": + this.camera.lookLeft(); + break; + case "lookRight": + this.camera.lookRight(); + break; } } animate() { if (this.activeMovements.size > 0) { - console.log("aaaaaaaaaaa"); [...this.activeMovements].forEach((movement) => this.move(movement)); } } @@ -269,43 +134,6 @@ export default class MovementsController { setupMouseMap(): () => void { const eventHandler = new ScreenSpaceEventHandler(this.scene.canvas); - const startLook = (click: { position: Cartesian2 }) => { - this.currentMousePosition = this.startMousePosition = - click.position.clone(); - this.activeMovements.add("look"); - }; - - const look = (movement: { endPosition: Cartesian2 }) => { - this.currentMousePosition = movement.endPosition.clone(); - }; - - const stopLook = () => { - this.activeMovements.delete("look"); - this.currentMousePosition = this.startMousePosition = undefined; - }; - - // User might try to turn while moving down (by pressing SHIFT) - // so trigger look event even when SHIFT is pressed. - eventHandler.setInputAction(startLook, ScreenSpaceEventType.LEFT_DOWN); - eventHandler.setInputAction( - startLook, - ScreenSpaceEventType.LEFT_DOWN, - KeyboardEventModifier.SHIFT - ); - - eventHandler.setInputAction(look, ScreenSpaceEventType.MOUSE_MOVE); - eventHandler.setInputAction( - look, - ScreenSpaceEventType.MOUSE_MOVE, - KeyboardEventModifier.SHIFT - ); - - eventHandler.setInputAction(stopLook, ScreenSpaceEventType.LEFT_UP); - eventHandler.setInputAction( - stopLook, - ScreenSpaceEventType.LEFT_UP, - KeyboardEventModifier.SHIFT - ); const mouseMapDestroyer = () => eventHandler.destroy(); return mouseMapDestroyer; } @@ -365,99 +193,6 @@ export default class MovementsController { } } -const sampleScratch = new Cartographic(); - -/** - * Sample the terrain height at the given position - */ -async function sampleTerrainHeight( - scene: Scene, - position: Cartesian3 -): Promise { - const terrainProvider = scene.terrainProvider; - if (terrainProvider instanceof EllipsoidTerrainProvider) return 0; - - const [sample] = await sampleTerrainMostDetailed(terrainProvider, [ - Cartographic.fromCartesian(position, scene.globe.ellipsoid, sampleScratch) - ]); - return sample.height; -} - -/** - * Sample the scene height at the given position - * - * Scene height is the maximum height of a tileset feature or any other entity - * at the given position. - */ -function sampleSceneHeight( - scene: Scene, - position: Cartesian3 -): number | undefined { - if (scene.sampleHeightSupported === false) return; - return scene.sampleHeight( - Cartographic.fromCartesian(position, undefined, sampleScratch) - ); -} - -/** - * Projects the {@vector} to the surface plane containing {@position} - * - * @param vector The input vector to project - * @param position The position used to determine the surface plane - * @param ellipsoid The ellipsoid used to compute the surface plane - * @returns The projection of {@vector} on the surface plane at the given {@position} - */ -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() - ); - const projectionOnSurface = Cartesian3.subtract( - vector, - projectionOnSurfaceNormal, - new Cartesian3() - ); - return projectionOnSurface; -} - -const rotateScratchQuaternion = new Quaternion(); -const rotateScratchMatrix = new Matrix3(); - -/** - * Rotates a vector about rotateAxis by rotateAmount - */ -function rotateVectorAboutAxis( - vector: Cartesian3, - rotateAxis: Cartesian3, - rotateAmount: number -) { - const quaternion = Quaternion.fromAxisAngle( - rotateAxis, - -rotateAmount, - rotateScratchQuaternion - ); - const rotation = Matrix3.fromQuaternion(quaternion, rotateScratchMatrix); - const rotatedVector = Matrix3.multiplyByVector( - rotation, - vector, - vector.clone() - ); - return rotatedVector; -} - // A regex matching input tag names const inputNodeRe = /input|textarea|select/i; diff --git a/lib/Styled/Icon.tsx b/lib/Styled/Icon.tsx index 8aaace722d0..d1eb2947ae1 100644 --- a/lib/Styled/Icon.tsx +++ b/lib/Styled/Icon.tsx @@ -42,6 +42,7 @@ export const GLYPHS = { help: require("../../wwwroot/images/icons/help.svg"), helpThick: require("../../wwwroot/images/icons/help-thick.svg"), increase: require("../../wwwroot/images/icons/increase.svg"), + keyboard: require("../../wwwroot/images/icons/keyboard.svg"), left: require("../../wwwroot/images/icons/left.svg"), lineChart: require("../../wwwroot/images/icons/line-chart.svg"), link: require("../../wwwroot/images/icons/link.svg"), diff --git a/wwwroot/images/icons/keyboard.svg b/wwwroot/images/icons/keyboard.svg new file mode 100644 index 00000000000..787b19cbc7b --- /dev/null +++ b/wwwroot/images/icons/keyboard.svg @@ -0,0 +1,60 @@ + + + + + + + + \ No newline at end of file diff --git a/wwwroot/images/keyboard_controls.svg b/wwwroot/images/keyboard_controls.svg new file mode 100644 index 00000000000..3b7842073d9 --- /dev/null +++ b/wwwroot/images/keyboard_controls.svg @@ -0,0 +1,262 @@ + + + + + graphic/keyboard_controls + + + + graphic/keyboard_controls + + + + + + + W + + + + D + + + + S + + + + A + + + + X + + + + Z + + + + + E + + + + Q + + + + R + + + + F + + + diff --git a/wwwroot/languages/en/translation.json b/wwwroot/languages/en/translation.json index b2275c6e2a0..cd9d644e0b9 100644 --- a/wwwroot/languages/en/translation.json +++ b/wwwroot/languages/en/translation.json @@ -2198,6 +2198,10 @@ "title": "Controls" } }, + "keyboardControls": { + "toolButtonTitle": "Keyboard navigation", + "header": "Keyboard map controls" + }, "notification": { "title": "Message" }, diff --git a/wwwroot/languages/it/translation.json b/wwwroot/languages/it/translation.json index 61b709330f8..c0c3a3cd4ad 100644 --- a/wwwroot/languages/it/translation.json +++ b/wwwroot/languages/it/translation.json @@ -1558,6 +1558,10 @@ "toolButtonTitle": "Modalità pedonale", "dropPedestrianTooltipMessage": "Clic sinistro per selezionare la posizione della goccia
Clic destro / Esc per annullare" }, + "keyboardControls": { + "toolButtonTitle": "Navigazione da tastiera", + "header": "Controlli mappa da tastiera" + }, "itemSearchTool": { "resetBtnText": "Cancella input", "searchBtnText": "Cerca", From 29c9225706faf8ffb644949065c07eb336bb5a59 Mon Sep 17 00:00:00 2001 From: glughi Date: Wed, 20 Nov 2024 11:44:58 +0100 Subject: [PATCH 3/3] Fix lint problems --- .../Map/MapNavigation/registerMapNavigations.tsx | 4 +++- lib/ReactViews/Tools/KeyboardMode/KeyboardMode.tsx | 6 +----- lib/ReactViews/Tools/KeyboardMode/MovementControls.tsx | 6 ++---- lib/ReactViews/Tools/KeyboardMode/MovementsController.ts | 8 +++----- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/lib/ReactViews/Map/MapNavigation/registerMapNavigations.tsx b/lib/ReactViews/Map/MapNavigation/registerMapNavigations.tsx index 8d8ec7667ae..a9b074d294e 100644 --- a/lib/ReactViews/Map/MapNavigation/registerMapNavigations.tsx +++ b/lib/ReactViews/Map/MapNavigation/registerMapNavigations.tsx @@ -30,7 +30,9 @@ import { ZOOM_CONTROL_ID } from "./Items"; import { TogglePickInfoController } from "./Items/TogglePickInfoTool"; -import KeyboardMode, { KEYBOARD_MODE_ID } from "../../Tools/KeyboardMode/KeyboardMode"; +import KeyboardMode, { + KEYBOARD_MODE_ID +} from "../../Tools/KeyboardMode/KeyboardMode"; export const CLOSE_TOOL_ID = "close-tool"; diff --git a/lib/ReactViews/Tools/KeyboardMode/KeyboardMode.tsx b/lib/ReactViews/Tools/KeyboardMode/KeyboardMode.tsx index e3ed212f081..5cdd3cf2bd0 100644 --- a/lib/ReactViews/Tools/KeyboardMode/KeyboardMode.tsx +++ b/lib/ReactViews/Tools/KeyboardMode/KeyboardMode.tsx @@ -6,7 +6,6 @@ import ViewState from "../../../ReactViewModels/ViewState"; import PositionRightOfWorkbench from "../../Workbench/PositionRightOfWorkbench"; import MovementControls from "./MovementControls"; - type KeyboardModeProps = { viewState: ViewState; }; @@ -24,9 +23,7 @@ const KeyboardMode: React.FC = observer((props) => { return ( - + ); }); @@ -38,5 +35,4 @@ const ControlsContainer = styled(PositionRightOfWorkbench)` bottom: 300px; `; - export default KeyboardMode; diff --git a/lib/ReactViews/Tools/KeyboardMode/MovementControls.tsx b/lib/ReactViews/Tools/KeyboardMode/MovementControls.tsx index 81ab5c43eba..ab22f89f11d 100644 --- a/lib/ReactViews/Tools/KeyboardMode/MovementControls.tsx +++ b/lib/ReactViews/Tools/KeyboardMode/MovementControls.tsx @@ -21,9 +21,7 @@ const MovementControls: React.FC = (props) => { const toggleMaximized = () => setIsMaximized(!isMaximized); useEffect(() => { - const movementsController = new MovementsController( - props.cesium - ); + const movementsController = new MovementsController(props.cesium); const detach = movementsController.activate(); return detach; /* eslint-disable-next-line react-hooks/exhaustive-deps */ @@ -70,7 +68,7 @@ const MinimizeMaximizeButton = styled(Button).attrs(({ maximized }) => ({ glyph={maximized ? Icon.GLYPHS.minimize : Icon.GLYPHS.maximize} /> ) -})) <{ maximized: boolean }>` +}))<{ maximized: boolean }>` padding: 0; margin: 0; border: 0; diff --git a/lib/ReactViews/Tools/KeyboardMode/MovementsController.ts b/lib/ReactViews/Tools/KeyboardMode/MovementsController.ts index a1edfe8e7c4..f73745ca123 100644 --- a/lib/ReactViews/Tools/KeyboardMode/MovementsController.ts +++ b/lib/ReactViews/Tools/KeyboardMode/MovementsController.ts @@ -31,9 +31,7 @@ export default class MovementsController { // Current active movements activeMovements: Set = new Set(); - constructor( - readonly cesium: Cesium, - ) { + constructor(readonly cesium: Cesium) { makeObservable(this); } @@ -139,8 +137,8 @@ export default class MovementsController { } /** - * Animate on each clock tick - */ + * Animate on each clock tick + */ startAnimating() { const stopAnimating = this.cesium.cesiumWidget.clock.onTick.addEventListener(