From d7ae8dbecf9e4134aa55fef4fe35030258ed9491 Mon Sep 17 00:00:00 2001 From: t0oF <93762994+w1nklr@users.noreply.github.com> Date: Mon, 7 Oct 2024 09:59:07 +0200 Subject: [PATCH] fix: camera vertical scale issue (#2292) Fix vertical scale not being taken into account on some camera manipulations. The camera controller now explicitly separates view states sent to DeckGL (containing the vertical scaling) from the ones manipulated by the user (without vertical scale). Fix target being stuck to Z = 0 when the vertical scale becomes 0. Update storybook example rotating the camera. --- .../subsurface-viewer/src/components/Map.tsx | 405 ++++++++++++------ .../CameraControlExamples.stories.tsx | 43 +- ...-examples-camera--rotate-camera-story.png} | Bin 211042 -> 211045 bytes 3 files changed, 307 insertions(+), 141 deletions(-) rename typescript/packages/subsurface-viewer/src/storybook/examples/__image_snapshots__/{subsurfaceviewer-examples-camera--reset-camera-story.png => subsurfaceviewer-examples-camera--rotate-camera-story.png} (93%) diff --git a/typescript/packages/subsurface-viewer/src/components/Map.tsx b/typescript/packages/subsurface-viewer/src/components/Map.tsx index 185dcc77f4..cc588422ef 100644 --- a/typescript/packages/subsurface-viewer/src/components/Map.tsx +++ b/typescript/packages/subsurface-viewer/src/components/Map.tsx @@ -730,12 +730,12 @@ const Map: React.FC = ({ const onViewStateChange = useCallback( // eslint-disable-next-line @typescript-eslint/no-explicit-any - ({ viewId, viewState }: { viewId: string; viewState: any }) => { - viewController.onViewStateChange(viewId, viewState); - if (getCameraPosition) { - getCameraPosition(viewState); - } - }, + ({ viewId, viewState }: { viewId: string; viewState: any }) => + viewController.onViewStateChange( + viewId, + viewState, + getCameraPosition + ), [getCameraPosition, viewController] ); @@ -971,9 +971,6 @@ function createLayer( /////////////////////////////////////////////////////////////////////////////////////////// // View Controller // Implements the algorithms to compute the views and the view state -type ScaledCamera = ViewStateType & { - scale: number; -}; type ViewControllerState = { // Explicit state triggerHome: number | undefined; @@ -986,23 +983,36 @@ type ViewControllerState = { }; type ViewControllerDerivedState = { // Derived state - scaledCameraClone: ScaledCamera | undefined; // clone of the camera - eventTarget: Point3D | undefined; // target set by events readyForInteraction: boolean; viewStateChanged: boolean; }; type ViewControllerFullState = ViewControllerState & ViewControllerDerivedState; +/** + * The `ViewController` class manages the state and interactions for a 3D view. + * It handles the rendering, view state updates, and synchronization of multiple views. + * + * @classdesc This class is responsible for managing the state of a 3D view, including + * camera settings, viewports, and interaction readiness. It provides methods + * to set targets, get views, and handle view state changes. + */ class ViewController { + /** + * Function to trigger a re-render. + */ private rerender_: React.DispatchWithoutAction; + /** + * Derived state for interaction readiness and view state changes. + */ private derivedState_: ViewControllerDerivedState = { - scaledCameraClone: undefined, - eventTarget: undefined, readyForInteraction: false, viewStateChanged: false, }; + /** + * Full state including camera settings, bounds, and deck size. + */ private state_: ViewControllerFullState = { triggerHome: undefined, camera: undefined, @@ -1020,68 +1030,113 @@ class ViewController { ...this.derivedState_, }; + /** + * The current views being managed. + */ private views_: ViewsType | undefined = undefined; + + /** + * The result object containing views and view states. + */ private result_: { + /** + * The views sent to DeckGL. + */ views: View[]; - viewState: Record; + + /** + * The view states sent to DeckGL, where the vertical scale has been applied. + */ + deckglViewStates: Record; + + /** + * The view states as known by the client code, without vertical scale. + */ + viewStates: Record; } = { views: [], - viewState: {}, + deckglViewStates: {}, + viewStates: {}, }; + /** + * Constructs a new instance of the Map component. + * + * @param rerender - A function to trigger a re-render of the component. + */ public constructor(rerender: React.DispatchWithoutAction) { this.rerender_ = rerender; } /** - * Sets the target from picks, which comes from the displayed - * scaled data. + * Sets the target from picks, which comes from the displayed scaled data. * @param target scaled 3D point. */ public readonly setScaledTarget = (target: Point3D) => { - this.derivedState_.eventTarget = [target[0], target[1], target[2]]; - this.rerender_(); + const vsKey = Object.keys(this.result_.deckglViewStates).at(0); + if (vsKey) { + // deep clone to notify change (memo checks object address) + this.result_.deckglViewStates = cloneDeep( + this.result_.deckglViewStates + ); + + // update target of deckglViewStates with the scaled event target + this.result_.deckglViewStates[vsKey].target = target; + this.result_.deckglViewStates[vsKey].transitionDuration = 1000; + // update target of deckglViewStates with the scaled event target + this.result_.viewStates[vsKey].target = inversedZScaled( + target, + this.state_.zScale, + this.result_.viewStates[vsKey].target + ); + + this.rerender_(); + } }; + /** + * Retrieves the views and their corresponding view states to be sent to DeckGL. + * @param views - The requested views. + * @param state - The current state. + * @returns The new views and their corresponding view states. + */ public readonly getViews = ( views: ViewsType | undefined, state: ViewControllerState ): [View[], Record] => { const fullState = this.consolidateState(state); - const newViews = this.getDeckGlViews(views, fullState); - const newViewState = this.getDeckGlViewState(views, fullState); + const newDeckglViews = this.getDeckGlViews(views, fullState); + const [newDeckglViewState, newViewStates] = + this.getDeckGlAndUserViewStates(views, fullState); // do not update this.state_ as it has not yet been applied - if (!isEmpty(newViewState)) { + if (!isEmpty(newDeckglViewState)) { this.state_ = fullState; } this.views_ = views; - this.result_.views = newViews; - this.result_.viewState = newViewState; - return [newViews, newViewState]; + this.result_.views = newDeckglViews; + this.result_.deckglViewStates = newDeckglViewState; + this.result_.viewStates = newViewStates; + return [newDeckglViews, newDeckglViewState]; }; - // consolidate "controlled" state (ie. set by parent) with "uncontrolled" state + /** + * Consolidates the controlled state (ie. set by parent) with the uncontrolled state. + * @param state - The current state. + * @returns The consolidated state. + */ private readonly consolidateState = ( state: ViewControllerState ): ViewControllerFullState => { - const fullState = { ...state, ...this.derivedState_ }; - if ( - fullState.camera != this.state_.camera || - !fullState.scaledCameraClone - ) { - // create a clone of the camera property to avoid editing it - fullState.scaledCameraClone = fullState.camera - ? { - ...(cloneDeep(fullState.camera) as ViewStateType), - scale: 1, - } - : undefined; - } - return fullState; + return { ...state, ...this.derivedState_ }; }; - // returns the DeckGL views (ie. view position and viewport) + /** + * Returns the DeckGL views (ie. view position and viewport) based on the input views and current state. + * @param views - The requested views. + * @param state - The current state. + * @returns The DeckGL views. + */ private readonly getDeckGlViews = ( views: ViewsType | undefined, state: ViewControllerFullState @@ -1094,20 +1149,20 @@ class ViewController { return buildDeckGlViews(views, state.deckSize); }; - // returns the DeckGL views state(s) (ie. camera settings applied to individual views) - private readonly getDeckGlViewState = ( + /** + * Returns the scaled DeckGL view state(s) (ie. camera settings applied to individual views) + * and the corresponding view states in user space (ie. not scaled by the zScale) + * based on the input views and current state. + * @param views requested views. + * @param state current state. + * @returns The DeckGL view states and the corresponding view states in user space. + */ + private readonly getDeckGlAndUserViewStates = ( views: ViewsType | undefined, state: ViewControllerFullState - ): Record => { + ): [Record, Record] => { const viewsChanged = views != this.views_; const triggerHome = state.triggerHome !== this.state_.triggerHome; - const updateTarget = - (viewsChanged || state.eventTarget !== this.state_.eventTarget) && - state.eventTarget != undefined; - // reset old zScale if new camera - if (state.camera != this.state_.camera) { - this.state_.zScale = 1; - } const updateZScale = viewsChanged || state.zScale !== this.state_?.zScale || triggerHome; const updateViewState = @@ -1118,82 +1173,76 @@ class ViewController { (!state.viewStateChanged && (state.boundingBox3d !== this.state_.boundingBox3d || state.deckSize != this.state_.deckSize)); - const needUpdate = updateZScale || updateTarget || updateViewState; + const needUpdate = updateZScale || updateViewState; - const isCacheEmpty = isEmpty(this.result_.viewState); + const isCacheEmpty = isEmpty(this.result_.deckglViewStates); if (!isCacheEmpty && !needUpdate) { - return this.result_.viewState; + return [this.result_.deckglViewStates, this.result_.viewStates]; } // initialize with last result - const prevViewState = this.result_.viewState; - let viewState = prevViewState; + const prevDeckglViewStates = this.result_.deckglViewStates; + let viewStates = this.result_.viewStates; + let deckglViewStates = this.result_.deckglViewStates; if (updateViewState || isCacheEmpty) { - viewState = buildDeckGlViewStates( + viewStates = buildViewStates( views, state.viewPortMargins, - state.scaledCameraClone, + state.camera, state.boundingBox3d, - state.zScale, state.bounds, state.deckSize ); + // create corresponding scaled states, to be handed over to DeckGL + deckglViewStates = buildScaledViewStates(viewStates, state.zScale); + // reset state this.derivedState_.readyForInteraction = canCameraBeDefined( - state.scaledCameraClone, + state.camera, state.boundingBox3d, state.bounds, state.deckSize ); this.derivedState_.viewStateChanged = false; + + return [deckglViewStates, viewStates]; } // check if view state could be computed - if (isEmpty(viewState)) { - return viewState; + if (isEmpty(viewStates)) { + return [deckglViewStates, viewStates]; } - const viewStateKeys = Object.keys(viewState); - if ( - updateTarget && - this.derivedState_.eventTarget && - viewStateKeys?.length === 1 - ) { - // deep clone to notify change (memo checks object address) - if (viewState === prevViewState) { - viewState = cloneDeep(prevViewState); - } - // update target - viewState[viewStateKeys[0]].target = this.derivedState_.eventTarget; - viewState[viewStateKeys[0]].transitionDuration = 1000; - // reset - this.derivedState_.eventTarget = undefined; - } if (updateZScale) { // deep clone to notify change (memo checks object address) - if (viewState === prevViewState) { - viewState = cloneDeep(prevViewState); + if (deckglViewStates === prevDeckglViewStates) { + deckglViewStates = cloneDeep(prevDeckglViewStates); } // Z scale to apply to target. - // - if triggerHome: the target was recomputed from the input data (ie. without any scale applied) - // - otherwise: previous scale (ie. this.state_.zScale) was already applied, and must be "reverted" - const targetScale = - state.zScale / - (triggerHome ? state.zScale : this.state_.zScale); // update target - for (const key in viewState) { - if (viewState[key].target) { - applyZScale(viewState[key].target, targetScale); + for (const key in deckglViewStates) { + if (deckglViewStates[key].target) { + deckglViewStates[key].target = zScaledTarget( + viewStates[key].target, + state.zScale + ); } } } - return viewState; + return [deckglViewStates, viewStates]; }; + /** + * Handles changes to the view state. + * @param viewId - The ID of the view. + * @param viewState - The new view state. + * @param getCameraPosition - A function to get the camera position. + */ public readonly onViewStateChange = ( viewId: string, - viewState: ViewStateType + viewState: ViewStateType, + getCameraPosition: ((input: ViewStateType) => void) | undefined ): void => { if (!this.derivedState_.readyForInteraction) { // disable interaction if the camera is not defined @@ -1203,26 +1252,56 @@ class ViewController { if (viewState.target?.length === 2) { // In orthographic mode viewState.target contains only x and y. Add existing z value. viewState.target.push( - this.result_.viewState[viewId].target?.[2] ?? 1 + this.result_.deckglViewStates[viewId].target?.[2] ?? 1 ); } const isSyncIds = viewports .filter((item) => item.isSync) .map((item) => item.id); if (isSyncIds?.includes(viewId)) { - const viewStateTable = this.views_?.viewports + const syncedViewStates = this.views_?.viewports .filter((item) => item.isSync) .map((item) => [item.id, viewState]); - const tempViewStates = Object.fromEntries(viewStateTable ?? []); - this.result_.viewState = { - ...this.result_.viewState, + const tempViewStates = Object.fromEntries(syncedViewStates ?? []); + this.result_.deckglViewStates = { + ...this.result_.deckglViewStates, ...tempViewStates, }; + // update corresponding view state + const keys = Object.keys(tempViewStates); + keys.forEach((key) => { + this.result_.viewStates = { + ...this.result_.viewStates, + [key]: { + ...tempViewStates[key], + target: inversedZScaled( + tempViewStates[key].target, + this.state_.zScale, + this.result_.viewStates[key].target + ), + }, + }; + }); } else { - this.result_.viewState = { - ...this.result_.viewState, + this.result_.deckglViewStates = { + ...this.result_.deckglViewStates, [viewId]: viewState, }; + // update corresponding view state + this.result_.viewStates = { + ...this.result_.viewStates, + [viewId]: { + ...viewState, + target: inversedZScaled( + viewState.target, + this.state_.zScale, + this.result_.viewStates[viewId].target + ), + }, + }; + } + if (getCameraPosition) { + getCameraPosition(this.result_.viewStates[viewId]); } this.derivedState_.viewStateChanged = true; this.rerender_(); @@ -1634,11 +1713,71 @@ function applyZScale( target: Point2D | Point3D | undefined, zScale: number ): void { - if (target?.[2]) { + if (target?.[2] != undefined) { target[2] = target[2] * zScale; } } +/** + * Returns a z-scaled target. + * This is needed, as the camera target is specified in world coordinates, while the Z scale + * is applied to the transformation matrix of the object coordinates. The target must be applied + * the same scale to be consistent with the display. + * @param target camera target that must take into account the Z scale. + * @param zScale Z scale. + */ +function zScaledTarget( + target: Point2D | Point3D | undefined, + zScale: number +): Point2D | Point3D | undefined { + if (!target) { + return undefined; + } + if (target[2] == undefined) { + return [target[0], target[1]]; + } + + return [target[0], target[1], target[2] * zScale]; +} + +/** + * Returns an inverted z-scaled target. + * This is needed, as the camera target is specified in world coordinates, while the Z scale + * is applied to the transformation matrix of the object coordinates. The target must be applied + * the same scale to be consistent with the display. + * @param target camera scaled target. + * @param zScale Z scale. + * @param unscaledTarget last known unscaled target which can be taken as a fallback. + */ +function inversedZScaled( + target: Point2D | Point3D | undefined, + zScale: number, + unscaledTarget?: Point2D | Point3D | undefined +): Point2D | Point3D | undefined { + if (!target) { + return undefined; + } + if (target[2] == undefined) { + return [target[0], target[1]]; + } + if (zScale != 0) { + return [target[0], target[1], target[2] / zScale]; + } + + if ( + unscaledTarget?.[2] != undefined && + target[0] === unscaledTarget?.[0] && + target[1] === unscaledTarget?.[1] + ) { + return [ + target[0], + target[1], + target[2] ? target[2] : unscaledTarget[2], + ]; + } + return [target[0], target[1], target[2]]; +} + /** * Returns the camera if it is fully specified (ie. the zoom is a valid number), otherwise computes * the zoom to visualize the complete camera boundingBox if set, the provided boundingBox otherwise. @@ -1649,15 +1788,10 @@ function applyZScale( function updateViewState( camera: ViewStateType, boundingBox: BoundingBox3D, - zScale: number, size: Size, is3D = true ): ViewStateType { if (isCameraDefined(camera)) { - if (is3D) { - // apply zScaling to target (target is in real coordinates while zScaling is applied to matrix transform) - applyZScale(camera.target, zScale); - } return camera; } @@ -1680,10 +1814,6 @@ function updateViewState( } if (!cameraHasTarget(camera)) { camera.target = boxCenter(boundingBox); - if (is3D) { - // apply zScaling to target (target is in real coordinates while zScaling is applied to matrix transform) - applyZScale(camera.target, zScale); - } } camera.minZoom = camera.minZoom ?? minZoom3D; camera.maxZoom = camera.maxZoom ?? maxZoom3D; @@ -1696,9 +1826,8 @@ function updateViewState( */ function computeViewState( viewPort: ViewportType, - scaledCamera: ScaledCamera | undefined, + scaledCamera: ViewStateType | undefined, boundingBox: BoundingBox3D | undefined, - zScale: number, bounds: BoundingBox2D | BoundsAccessor | undefined, viewportMargins: MarginsType, views: ViewsType | undefined, @@ -1718,12 +1847,7 @@ function computeViewState( if (viewPort.show3D ?? false) { // If the camera is defined, use it if (isCameraPositionDefined) { - return updateViewState( - scaledCamera, - boundingBox, - zScale / (scaledCamera.scale || 1), - size - ); + return updateViewState(scaledCamera, boundingBox, size); } // deprecated in 3D, kept for backward compatibility @@ -1744,19 +1868,13 @@ function computeViewState( rotationX: 45, // look down z -axis at 45 degrees rotationOrbit: 0, }; - return updateViewState(defaultCamera, boundingBox, zScale, size); + return updateViewState(defaultCamera, boundingBox, size); } else { const is3D = false; // If the camera is defined, use it if (isCameraPositionDefined) { - return updateViewState( - scaledCamera, - boundingBox, - zScale, - size, - is3D - ); + return updateViewState(scaledCamera, boundingBox, size, is3D); } const centerOfData: Point3D = boxCenter(boundingBox); @@ -1788,12 +1906,23 @@ function computeViewState( } } -function buildDeckGlViewStates( +/** + * Builds the view states for the DeckGL views. + * These view states are in user space (ie. not scaled by the zScale). + * @param views requested views. + * @param viewPortMargins margin between the viewports. + * @param scaledCamera camera to apply to the views. + * @param boundingBox data bounding box, used if no target nor bounding box is specified by the camera + * @param zScale vertical scale. + * @param bounds displayed 2D bounds in 2D view. + * @param size deck component size. + * @returns + */ +function buildViewStates( views: ViewsType | undefined, viewPortMargins: MarginsType, - scaledCamera: ScaledCamera | undefined, + scaledCamera: ViewStateType | undefined, boundingBox: BoundingBox3D | undefined, - zScale: number, bounds: BoundingBox2D | BoundsAccessor | undefined, size: Size ): Record { @@ -1812,7 +1941,6 @@ function buildDeckGlViewStates( views.viewports[0], scaledCamera, boundingBox, - zScale, bounds, viewPortMargins, views, @@ -1838,7 +1966,6 @@ function buildDeckGlViewStates( currentViewport, scaledCamera, boundingBox, - zScale, bounds, viewPortMargins, views, @@ -1855,6 +1982,28 @@ function buildDeckGlViewStates( return result; } +/** + * Builds the scaled view states for the DeckGL views. + * These view states are scaled by the zScale. + * They shared all the fields except the scaled target with the provided view states. + * @param viewStates view states in user space. + * @param zScale vertical scale. + * @returns + */ +function buildScaledViewStates( + viewStates: Record, + zScale: number +): Record { + const result: Record = cloneDeep(viewStates); + for (const key in result) { + const viewState = result[key]; + if (viewState) { + applyZScale(viewState.target, zScale); + } + } + return result; +} + function handleMouseEvent( type: "click" | "hover", infos: PickingInfo[], diff --git a/typescript/packages/subsurface-viewer/src/storybook/examples/CameraControlExamples.stories.tsx b/typescript/packages/subsurface-viewer/src/storybook/examples/CameraControlExamples.stories.tsx index 56faf8b9d7..5d8ea584ae 100644 --- a/typescript/packages/subsurface-viewer/src/storybook/examples/CameraControlExamples.stories.tsx +++ b/typescript/packages/subsurface-viewer/src/storybook/examples/CameraControlExamples.stories.tsx @@ -1,3 +1,5 @@ +import { cloneDeep } from "lodash"; + import type { Meta, StoryObj } from "@storybook/react"; import { userEvent } from "@storybook/test"; import React from "react"; @@ -452,20 +454,35 @@ const ResetCameraPropertyDefaultCameraPosition = { }; const ResetCameraComponent: React.FC = (args) => { - const [camera, setCamera] = React.useState( - () => args.cameraPosition ?? ResetCameraPropertyDefaultCameraPosition - ); + const currentCameraRef = React.useRef(null); - const handleChange = () => { - setCamera({ - ...camera, - rotationOrbit: camera.rotationOrbit + 5, - }); - }; + const initialCamera = React.useMemo(() => { + currentCameraRef.current = cloneDeep( + args.cameraPosition ?? ResetCameraPropertyDefaultCameraPosition + ); + return currentCameraRef.current; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [args.cameraPosition, args.triggerHome]); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_camera, setCamera] = React.useState(() => initialCamera); + + const onCameraChanged = React.useCallback((camera: ViewStateType) => { + currentCameraRef.current = camera; + }, []); + + const rotateCamera = React.useCallback(() => { + currentCameraRef.current = cloneDeep(currentCameraRef.current); + if (currentCameraRef.current) { + currentCameraRef.current.rotationOrbit += 5; + setCamera(currentCameraRef.current); + } + }, []); const props = { ...args, - cameraPosition: camera, + getCameraPosition: onCameraChanged, + cameraPosition: currentCameraRef.current ?? undefined, }; return ( @@ -473,14 +490,14 @@ const ResetCameraComponent: React.FC = (args) => {
- + ); }; -export const ResetCameraStory: StoryObj = { +export const RotateCameraStory: StoryObj = { args: { - id: "ResetCameraProperty", + id: "RotateCameraProperty", layers: [ huginAxes3DLayer, hugin25mKhNetmapMapLayerPng, diff --git a/typescript/packages/subsurface-viewer/src/storybook/examples/__image_snapshots__/subsurfaceviewer-examples-camera--reset-camera-story.png b/typescript/packages/subsurface-viewer/src/storybook/examples/__image_snapshots__/subsurfaceviewer-examples-camera--rotate-camera-story.png similarity index 93% rename from typescript/packages/subsurface-viewer/src/storybook/examples/__image_snapshots__/subsurfaceviewer-examples-camera--reset-camera-story.png rename to typescript/packages/subsurface-viewer/src/storybook/examples/__image_snapshots__/subsurfaceviewer-examples-camera--rotate-camera-story.png index 5e97a9ecedbee31b5e3f36fc3cfb0bcee96e1157..ad9de3d26b167dd4b4b6c29d7e3537db41f9ef75 100644 GIT binary patch delta 13814 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 E0kN}BYybcN delta 13816 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