diff --git a/libraries/mapping/engines/cesium/src/lib/CustomViewer/CustomViewer.tsx b/libraries/mapping/engines/cesium/src/lib/CustomViewer/CustomViewer.tsx index bc958147..6e2b7e23 100644 --- a/libraries/mapping/engines/cesium/src/lib/CustomViewer/CustomViewer.tsx +++ b/libraries/mapping/engines/cesium/src/lib/CustomViewer/CustomViewer.tsx @@ -5,19 +5,13 @@ import { useContext, useEffect, useRef, - useState, } from "react"; import { useLocation } from "react-router-dom"; -import { useDispatch } from "react-redux"; import { - BoundingSphere, - Cartesian3, + Color, HeadingPitchRange, - Math as CeMath, - OrthographicFrustum, - PerspectiveFrustum, Rectangle, Viewer, } from "cesium"; @@ -25,29 +19,21 @@ import { Viewer as ResiumViewer } from "resium"; import { TopicMapContext } from "react-cismap/contexts/TopicMapContextProvider"; -import { useTweakpaneCtx } from "@carma-commons/debug"; - import { - setIsAnimating, - setScreenSpaceCameraControllerEnableCollisionDetection, - setScreenSpaceCameraControllerMaximumZoomDistance, - setScreenSpaceCameraControllerMinimumZoomDistance, - useScreenSpaceCameraControllerEnableCollisionDetection, - useScreenSpaceCameraControllerMaximumZoomDistance, - useScreenSpaceCameraControllerMinimumZoomDistance, useShowSecondaryTileset, useViewerHome, useViewerHomeOffset, useViewerIsMode2d, } from "../CustomViewerContextProvider/slices/cesium"; -import { cameraToCartographicDegrees, pickViewerCanvasCenter, resolutionFractions } from "../utils"; -import { formatFractions } from "../utils/formatters"; +import { cameraToCartographicDegrees } from "../utils"; import { useCesiumCustomViewer } from "../CustomViewerContextProvider"; import { BaseTilesets } from "./components/BaseTilesets"; import { encodeScene, replaceHashRoutedHistory } from "./utils"; import { useInitializeViewer, useLogCesiumRenderIn2D } from "./hooks"; import useTransitionTimeout from "./hooks/useTransitionTimeout"; import useDisableSSCC from "./hooks/useDisableSSCC"; +import useTweakpane from "./hooks/useTweakpane"; +import useCameraLimiter from './hooks/useCameraLimiter'; type CustomViewerProps = { @@ -88,13 +74,9 @@ type CustomViewerProps = { const DEFAULT_RESOLUTION_SCALE = 1; export const TRANSITION_DELAY = 1000; -const CESIUM_CAMERA_MIN_PITCH = CeMath.toRadians(-20); -const CESIUM_CAMERA_MIN_PITCH_RESET_TO = CeMath.toRadians(-30); - function CustomViewer(props: CustomViewerProps) { const { viewer, setViewer, imageryLayer } = useCesiumCustomViewer(); - const dispatch = useDispatch(); const home = useViewerHome(); const homeOffset = useViewerHomeOffset(); const isSecondaryStyle = useShowSecondaryTileset(); @@ -118,23 +100,10 @@ function CustomViewer(props: CustomViewerProps) { enableLocationHashUpdate = true, } = props; - const [viewportLimit, setViewportLimit] = useState(4); - const [viewportLimitDebug, setViewportLimitDebug] = useState(false); - const minZoomDistance = useScreenSpaceCameraControllerMinimumZoomDistance(); - const maxZoomDistance = useScreenSpaceCameraControllerMaximumZoomDistance(); - const collisions = useScreenSpaceCameraControllerEnableCollisionDetection(); - const setMaxZoomDistance = (v: number) => dispatch(setScreenSpaceCameraControllerMaximumZoomDistance(v)); - const setMinZoomDistance = (v: number) => dispatch(setScreenSpaceCameraControllerMinimumZoomDistance(v)); - const setCollisions = (v: boolean) => dispatch(setScreenSpaceCameraControllerEnableCollisionDetection(v)); - - const previousViewerRef = useRef(null); // track viewer changes const previousIsMode2d = useRef(null); const previousIsSecondaryStyle = useRef(null); - const baseResolutionScale = viewerOptions.resolutionScale || DEFAULT_RESOLUTION_SCALE; - const [adaptiveResolutionScale, setAdaptiveResolutionScale] = useState(baseResolutionScale); - // eslint-disable-next-line @typescript-eslint/no-explicit-any const topicMapContext: any = useContext(TopicMapContext); @@ -142,229 +111,8 @@ function CustomViewer(props: CustomViewerProps) { const leaflet = topicMapContext?.routedMapRef?.leafletMap?.leafletElement; - const [isUserAction, setIsUserAction] = useState(false); // DEV TWEAKPANE - - - // Not neede anymore - /* - useEffect(() => { - if (topicMapContext && isMode2d) { - const handleLeafletMoveEnd = (event: LeafletEvent | { target: LeafletMap }) => { - const center = event.target?.getCenter(); - const zoom = event.target?.getZoom(); - - const mapLocation = { - lat: center.lat, - lng: center.lng, - zoom, - }; - - - if (viewer && mapLocation.lat && mapLocation.lng && mapLocation.zoom) { - console.log("HOOK: zoom handleLeafletMoveEnd", mapLocation); - //leafletToCesiumCamera(viewer, mapLocation); - } - }; - - console.log("HOOK: topicMapContext changed", topicMapContext); - const leaflet = - topicMapContext?.routedMapRef?.leafletMap?.leafletElement; - if (leaflet) { - console.log("HOOK: leaflet changed", leaflet); - } - if (leaflet) { - console.log("HOOK: [CustomViewer] zoom initial position from leaflet") - handleLeafletMoveEnd({ target: leaflet }); - leaflet.on("moveend", handleLeafletMoveEnd); - return () => { - leaflet.off("moveend", handleLeafletMoveEnd); - }; - } - - } - }, [topicMapContext, viewer, isMode2d]); - */ - - - useTweakpaneCtx( - { - title: "Camera Settings", - }, - { - get fov() { - return (viewer?.scene.camera.frustum as PerspectiveFrustum)?.fov || 1.0; - }, - - set fov(value: number) { - if ( - viewer && - viewer.scene.camera.frustum instanceof PerspectiveFrustum && - !Number.isNaN(value) - ) { - viewer.scene.camera.frustum.fov = value; - } - }, - get orthographic() { - return viewer?.scene.camera.frustum instanceof OrthographicFrustum; - }, - set orthographic(value: boolean) { - if (viewer) { - if ( - value && - viewer.scene.camera.frustum instanceof PerspectiveFrustum - ) { - viewer.scene.camera.switchToOrthographicFrustum(); - } else if ( - viewer.scene.camera.frustum instanceof OrthographicFrustum - ) { - viewer.scene.camera.switchToPerspectiveFrustum(); - } - } - }, - }, - - [ - { - name: "fov", - label: "FOV", - min: Math.PI / 400, - max: Math.PI, - step: 0.01, - format: (v) => `${parseFloat(CeMath.toDegrees(v).toFixed(2))}°`, - }, - { - name: "orthographic", - label: "Orthographic", - type: "boolean", - }, - ], - ); - - useTweakpaneCtx( - { - title: "Scene Settings", - }, - { - get viewportLimitDebug() { - return viewportLimitDebug; - }, - set viewportLimitDebug(value: boolean) { - setViewportLimitDebug(value); - }, - get viewportLimit() { - return viewportLimit; - }, - set viewportLimit(value: number) { - !Number.isNaN(value) && setViewportLimit(value); - }, - get resolutionScale() { - // Find the closest value in the array to the current resolutionScale and return its index - const currentValue = viewer ? viewer.resolutionScale : 1; - const closestIndex = resolutionFractions.findIndex( - (value) => value === currentValue, - ); - return closestIndex !== -1 - ? closestIndex - : resolutionFractions.length - 1; // Default to the last index if not found - }, - set resolutionScale(index) { - // Use the index to set the resolutionScale from the array - if (viewer && index >= 0 && index < resolutionFractions.length) { - const value = resolutionFractions[index]; - viewer.resolutionScale = value; - } - }, - }, - - [ - { name: "viewportLimit", min: 1.5, max: 10, step: 0.5 }, - { name: "viewportLimitDebug" }, - { - name: "resolutionScale", - min: 0, // The minimum index - max: resolutionFractions.length - 1, // The maximum index - step: 1, // Step by index - format: (v: number) => formatFractions(resolutionFractions[v]), - }, - ], - ); - - useTweakpaneCtx( - { - title: "Scene Camera Controller", - }, - { - get maxZoomDistance() { - return maxZoomDistance - }, - set maxZoomDistance(value: number) { - if (!isNaN(value)) { - // TODO add debounce for all Setters - setMaxZoomDistance(value) - } - }, - get minZoomDistance() { - return minZoomDistance - }, - set minZoomDistance(value: number) { - if (!isNaN(value)) { - setMinZoomDistance(value) - } - }, - - get collisions() { - return collisions - }, - set collisions(v: boolean) { - setCollisions(v) - } - }, - [ - { name: "collisions" }, - { name: "maxZoomDistance", min: 1000, max: 1000000, step: 1000 }, - { name: "minZoomDistance", min: 10, max: 1000, step: 10 }, - ], - ); - - useLogCesiumRenderIn2D(); - useTransitionTimeout(); - useDisableSSCC(); - - /* - useEffect(() => { - if (!viewer) return; - console.log("[CESIUM] HOOK Track user focus") - - const canvas = viewer.canvas; - - // Ensure the canvas can receive focus - canvas.setAttribute("tabindex", "0"); - - // Event handlers - const handleFocus = () => setIsUserAction(true); - const handleBlur = () => setIsUserAction(false); - const handleMouseDown = () => { - canvas.focus(); - setIsUserAction(true); - }; - - // Add event listeners - canvas.addEventListener("focus", handleFocus); - canvas.addEventListener("blur", handleBlur); - canvas.addEventListener("mousedown", handleMouseDown); - canvas.addEventListener("mousemove", handleMouseDown); // Track mouse move as interaction - - // Cleanup event listeners on unmount - return () => { - canvas.removeEventListener("focus", handleFocus); - canvas.removeEventListener("blur", handleBlur); - canvas.removeEventListener("mousedown", handleMouseDown); - canvas.removeEventListener("mousemove", handleMouseDown); - }; - }, [viewer]); - */ - + useTweakpane() const viewerRef = useCallback((node) => { if (node !== null) { @@ -377,6 +125,11 @@ function CustomViewer(props: CustomViewerProps) { useInitializeViewer(viewer, home, homeOffset, leaflet); + useLogCesiumRenderIn2D(); + useTransitionTimeout(); + useDisableSSCC(); + useCameraLimiter(); + useEffect(() => { if (viewer && enableLocationHashUpdate && !isMode2d) { console.log( @@ -476,63 +229,6 @@ function CustomViewer(props: CustomViewerProps) { isMode2d, ]); - useEffect(() => { - if (viewer) { - console.log("HOOK [2D3D|CESIUM] viewer changed add new Cesium MoveEnd Listener to reset rolled camera"); - const moveEndListener = async () => { - console.log("HOOK [2D3D|CESIUM] xxx", viewer.camera.pitch, isMode2d); - if (viewer.camera.position && !isMode2d) { - console.log("HOOK [2D3D|CESIUM] xxx", viewer.camera.pitch); - const rollDeviation = - Math.abs(CeMath.TWO_PI - viewer.camera.roll) % CeMath.TWO_PI; - - if (rollDeviation > 0.02) { - console.log("LISTENER HOOK [2D3D|CESIUM|CAMERA]: flyTo reset roll 2D3D", rollDeviation); - const duration = Math.min(rollDeviation, 1); - dispatch(setIsAnimating(true)); - viewer.camera.flyTo({ - destination: viewer.camera.position, - orientation: { - heading: viewer.camera.heading, - pitch: viewer.camera.pitch, - roll: 0, - }, - duration, - complete: () => dispatch(setIsAnimating(false)) - }); - } - const preventLookingUp = collisions && viewer.camera.pitch > CESIUM_CAMERA_MIN_PITCH; - if (preventLookingUp && collisions) { - console.log("LISTENER HOOK [2D3D|CESIUM|CAMERA]: reset pitch", viewer.camera.pitch); - const centerPos = pickViewerCanvasCenter(viewer).scenePosition; - if (centerPos) { - dispatch(setIsAnimating(true)); - const distance = Cartesian3.distance(centerPos, viewer.camera.position) - viewer.camera.flyToBoundingSphere( - new BoundingSphere(centerPos, distance), - { - offset: { - heading: viewer.camera.heading, - pitch: CESIUM_CAMERA_MIN_PITCH_RESET_TO, - range: distance, - }, - duration: 1.5, - complete: () => dispatch(setIsAnimating(false)) - } - ); - } - } - }; - } - viewer.camera.moveEnd.addEventListener(moveEndListener); - return () => { - viewer.camera.moveEnd.removeEventListener(moveEndListener); - }; - } - - }, [viewer, collisions, isMode2d, dispatch]); - - useEffect(() => { // update hash hook if (viewer) { @@ -560,7 +256,6 @@ function CustomViewer(props: CustomViewerProps) { enableLocationHashUpdate, ]); - console.info("RENDER: [CESIUM] CustomViewer"); return ( diff --git a/libraries/mapping/engines/cesium/src/lib/CustomViewer/hooks/useCameraLimiter.ts b/libraries/mapping/engines/cesium/src/lib/CustomViewer/hooks/useCameraLimiter.ts new file mode 100644 index 00000000..20ef3882 --- /dev/null +++ b/libraries/mapping/engines/cesium/src/lib/CustomViewer/hooks/useCameraLimiter.ts @@ -0,0 +1,89 @@ +import { useEffect } from "react"; +import { useDispatch } from "react-redux"; +import { BoundingSphere, Cartesian3, Math as CeMath } from "cesium"; + +import { + setIsAnimating, + useCesiumCustomViewer, + useScreenSpaceCameraControllerEnableCollisionDetection, + useViewerIsMode2d, +} from "../../CustomViewerContextProvider"; +import { pickViewerCanvasCenter } from "../../utils/cesiumHelpers"; + +const CESIUM_CAMERA_MIN_PITCH = CeMath.toRadians(-20); +const CESIUM_CAMERA_MIN_PITCH_RESET_TO = CeMath.toRadians(-30); + +const useCameraLimiter = () => { + const { viewer } = useCesiumCustomViewer(); + const dispatch = useDispatch(); + const isMode2d = useViewerIsMode2d(); + const collisions = useScreenSpaceCameraControllerEnableCollisionDetection(); + useEffect(() => { + if (viewer) { + console.log( + "HOOK [2D3D|CESIUM] viewer changed add new Cesium MoveEnd Listener to reset rolled camera", + ); + const moveEndListener = async () => { + console.log("HOOK [2D3D|CESIUM] xxx", viewer.camera.pitch, isMode2d); + if (viewer.camera.position && !isMode2d) { + console.log("HOOK [2D3D|CESIUM] xxx", viewer.camera.pitch); + const rollDeviation = + Math.abs(CeMath.TWO_PI - viewer.camera.roll) % CeMath.TWO_PI; + + if (rollDeviation > 0.02) { + console.log( + "LISTENER HOOK [2D3D|CESIUM|CAMERA]: flyTo reset roll 2D3D", + rollDeviation, + ); + const duration = Math.min(rollDeviation, 1); + dispatch(setIsAnimating(true)); + viewer.camera.flyTo({ + destination: viewer.camera.position, + orientation: { + heading: viewer.camera.heading, + pitch: viewer.camera.pitch, + roll: 0, + }, + duration, + complete: () => dispatch(setIsAnimating(false)), + }); + } + const preventLookingUp = + collisions && viewer.camera.pitch > CESIUM_CAMERA_MIN_PITCH; + if (preventLookingUp && collisions) { + console.log( + "LISTENER HOOK [2D3D|CESIUM|CAMERA]: reset pitch", + viewer.camera.pitch, + ); + const centerPos = pickViewerCanvasCenter(viewer).scenePosition; + if (centerPos) { + dispatch(setIsAnimating(true)); + const distance = Cartesian3.distance( + centerPos, + viewer.camera.position, + ); + viewer.camera.flyToBoundingSphere( + new BoundingSphere(centerPos, distance), + { + offset: { + heading: viewer.camera.heading, + pitch: CESIUM_CAMERA_MIN_PITCH_RESET_TO, + range: distance, + }, + duration: 1.5, + complete: () => dispatch(setIsAnimating(false)), + }, + ); + } + } + } + }; + viewer.camera.moveEnd.addEventListener(moveEndListener); + return () => { + viewer.camera.moveEnd.removeEventListener(moveEndListener); + }; + } + }, [viewer, collisions, isMode2d, dispatch]); +}; + +export default useCameraLimiter; diff --git a/libraries/mapping/engines/cesium/src/lib/CustomViewer/hooks/useDisableSSCC.ts b/libraries/mapping/engines/cesium/src/lib/CustomViewer/hooks/useDisableSSCC.ts index 357e7468..ecf9301f 100644 --- a/libraries/mapping/engines/cesium/src/lib/CustomViewer/hooks/useDisableSSCC.ts +++ b/libraries/mapping/engines/cesium/src/lib/CustomViewer/hooks/useDisableSSCC.ts @@ -1,4 +1,3 @@ -import type { Viewer } from "cesium"; import { useEffect } from "react"; import { useCesiumCustomViewer, diff --git a/libraries/mapping/engines/cesium/src/lib/CustomViewer/hooks/useTweakpane.ts b/libraries/mapping/engines/cesium/src/lib/CustomViewer/hooks/useTweakpane.ts new file mode 100644 index 00000000..fb06fb34 --- /dev/null +++ b/libraries/mapping/engines/cesium/src/lib/CustomViewer/hooks/useTweakpane.ts @@ -0,0 +1,165 @@ +import { + Math as CeMath, + OrthographicFrustum, + PerspectiveFrustum, + Viewer, +} from "cesium"; + +import { useTweakpaneCtx } from "@carma-commons/debug"; +import { formatFractions } from "../../utils/formatters"; +import { resolutionFractions } from "../../utils/cesiumHelpers"; +import { + setScreenSpaceCameraControllerEnableCollisionDetection, + setScreenSpaceCameraControllerMaximumZoomDistance, + setScreenSpaceCameraControllerMinimumZoomDistance, + useCesiumCustomViewer, + useScreenSpaceCameraControllerEnableCollisionDetection, + useScreenSpaceCameraControllerMaximumZoomDistance, + useScreenSpaceCameraControllerMinimumZoomDistance, +} from "../../CustomViewerContextProvider"; +import { useDispatch } from "react-redux"; + +const useTweakpane = () => { + const { viewer } = useCesiumCustomViewer(); + + const dispatch = useDispatch(); + + const minZoomDistance = useScreenSpaceCameraControllerMinimumZoomDistance(); + const maxZoomDistance = useScreenSpaceCameraControllerMaximumZoomDistance(); + const collisions = useScreenSpaceCameraControllerEnableCollisionDetection(); + + const setMaxZoomDistance = (v: number) => + dispatch(setScreenSpaceCameraControllerMaximumZoomDistance(v)); + const setMinZoomDistance = (v: number) => + dispatch(setScreenSpaceCameraControllerMinimumZoomDistance(v)); + const setCollisions = (v: boolean) => + dispatch(setScreenSpaceCameraControllerEnableCollisionDetection(v)); + + useTweakpaneCtx( + { + title: "Camera Settings", + }, + { + get fov() { + return (viewer?.scene.camera.frustum as PerspectiveFrustum)?.fov || 1.0; + }, + + set fov(value: number) { + if ( + viewer && + viewer.scene.camera.frustum instanceof PerspectiveFrustum && + !Number.isNaN(value) + ) { + viewer.scene.camera.frustum.fov = value; + } + }, + get orthographic() { + return viewer?.scene.camera.frustum instanceof OrthographicFrustum; + }, + set orthographic(value: boolean) { + if (viewer) { + if ( + value && + viewer.scene.camera.frustum instanceof PerspectiveFrustum + ) { + viewer.scene.camera.switchToOrthographicFrustum(); + } else if ( + viewer.scene.camera.frustum instanceof OrthographicFrustum + ) { + viewer.scene.camera.switchToPerspectiveFrustum(); + } + } + }, + }, + + [ + { + name: "fov", + label: "FOV", + min: Math.PI / 400, + max: Math.PI, + step: 0.01, + format: (v) => `${parseFloat(CeMath.toDegrees(v).toFixed(2))}°`, + }, + { + name: "orthographic", + label: "Orthographic", + type: "boolean", + }, + ], + ); + + useTweakpaneCtx( + { + title: "Scene Settings", + }, + { + get resolutionScale() { + // Find the closest value in the array to the current resolutionScale and return its index + const currentValue = viewer ? viewer.resolutionScale : 1; + const closestIndex = resolutionFractions.findIndex( + (value) => value === currentValue, + ); + return closestIndex !== -1 + ? closestIndex + : resolutionFractions.length - 1; // Default to the last index if not found + }, + set resolutionScale(index) { + // Use the index to set the resolutionScale from the array + if (viewer && index >= 0 && index < resolutionFractions.length) { + const value = resolutionFractions[index]; + viewer.resolutionScale = value; + } + }, + }, + + [ + { + name: "resolutionScale", + min: 0, // The minimum index + max: resolutionFractions.length - 1, // The maximum index + step: 1, // Step by index + format: (v: number) => formatFractions(resolutionFractions[v]), + }, + ], + ); + + useTweakpaneCtx( + { + title: "Scene Camera Controller", + }, + { + get maxZoomDistance() { + return maxZoomDistance; + }, + set maxZoomDistance(value: number) { + if (!isNaN(value)) { + // TODO add debounce for all Setters + setMaxZoomDistance(value); + } + }, + get minZoomDistance() { + return minZoomDistance; + }, + set minZoomDistance(value: number) { + if (!isNaN(value)) { + setMinZoomDistance(value); + } + }, + + get collisions() { + return collisions; + }, + set collisions(v: boolean) { + setCollisions(v); + }, + }, + [ + { name: "collisions" }, + { name: "maxZoomDistance", min: 1000, max: 1000000, step: 1000 }, + { name: "minZoomDistance", min: 10, max: 1000, step: 10 }, + ], + ); +}; + +export default useTweakpane;