diff --git a/typescript/packages/subsurface-viewer/src/components/Map.tsx b/typescript/packages/subsurface-viewer/src/components/Map.tsx index 3f89b963e..8498547d1 100644 --- a/typescript/packages/subsurface-viewer/src/components/Map.tsx +++ b/typescript/packages/subsurface-viewer/src/components/Map.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import React, { useEffect, useState, useCallback, useMemo } from "react"; import type { Feature, FeatureCollection } from "geojson"; @@ -63,6 +62,7 @@ import { WellsLayer, Axes2DLayer, NorthArrow3DLayer } from "../layers"; import IntersectionView from "../views/intersectionView"; import type { Unit } from "convert-units"; import type { LightsType } from "../SubsurfaceViewer"; +import { getZoom, useLateralZoom } from "../utils/camera"; /** * 3D bounding box defined as [xmin, ymin, zmin, xmax, ymax, zmax]. @@ -91,8 +91,6 @@ const maxZoom2D = 4; type ArrowEvent = { key: "ArrowUp" | "ArrowDown" | "PageUp" | "PageDown"; shiftModifier: boolean; - // altModifier: boolean; - // ctrlModifier: boolean; }; function updateZScaleReducer(zScale: number, action: ArrowEvent): number { @@ -258,6 +256,7 @@ export interface ViewportType { zoom?: number; rotationX?: number; rotationOrbit?: number; + verticalScale?: number; isSync?: boolean; } @@ -267,7 +266,7 @@ export interface ViewportType { */ export interface ViewStateType { target: number[]; - zoom: number | BoundingBox3D | undefined; + zoom: number | [number, number] | BoundingBox3D | undefined; rotationX: number; rotationOrbit: number; minZoom?: number; @@ -835,6 +834,8 @@ const Map: React.FC = ({ viewController, ]); + const lateralZoom = useLateralZoom(deckGlViewState); + if (!deckGlViews || isEmpty(deckGlViews) || isEmpty(deckGLLayers)) return null; return ( @@ -898,10 +899,7 @@ const Map: React.FC = ({ {scale?.visible ? ( @@ -1026,6 +1024,7 @@ function createLayer( configuration: JSONConfiguration ): Layer | null { const typeKey = configuration.typeKey; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const classes = configuration.classes as Record; if (layerData[typeKey]) { const type = layerData[typeKey] as string; @@ -1432,7 +1431,7 @@ function getViewStateFromBounds( const view_state: ViewStateType = { target: viewPort.target ?? fb_target, - zoom: viewPort.zoom ?? fb_zoom, + zoom: getZoom(viewPort, fb_zoom), rotationX: 90, // look down z -axis rotationOrbit: 0, minZoom: viewPort.show3D ? minZoom3D : minZoom2D, @@ -1447,7 +1446,8 @@ type ViewTypeType = | typeof OrbitView | typeof IntersectionView | typeof OrthographicView; -function getVT( + +function getViewType( viewport: ViewportType ): [ ViewType: ViewTypeType, @@ -1486,7 +1486,7 @@ function newView( const far = 9999; const near = viewport.show3D ? 0.1 : -9999; - const [ViewType, Controller] = getVT(viewport); + const [ViewType, Controller] = getViewType(viewport); return new ViewType({ id: viewport.id, controller: { 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 1be8d6e6e..6003ace10 100644 --- a/typescript/packages/subsurface-viewer/src/storybook/examples/CameraControlExamples.stories.tsx +++ b/typescript/packages/subsurface-viewer/src/storybook/examples/CameraControlExamples.stories.tsx @@ -1,5 +1,5 @@ -import React from "react"; import type { Meta, StoryObj } from "@storybook/react"; +import React from "react"; import { SimpleMeshLayer } from "@deck.gl/mesh-layers/typed"; import { SphereGeometry } from "@luma.gl/engine"; @@ -8,26 +8,28 @@ import Box from "@mui/material/Box"; import Slider from "@mui/material/Slider"; import { styled } from "@mui/material/styles"; -import SubsurfaceViewer from "../../SubsurfaceViewer"; import type { SubsurfaceViewerProps } from "../../SubsurfaceViewer"; -import type { ViewStateType, BoundingBox3D } from "../../components/Map"; -import { AxesLayer } from "../../layers"; +import SubsurfaceViewer from "../../SubsurfaceViewer"; +import type { BoundingBox3D, ViewStateType } from "../../components/Map"; +import { Axes2DLayer, AxesLayer } from "../../layers"; import { - mainStyle, - huginAxes3DLayer, + customLayerWithPolygonDataProps, default2DViews, default3DViews, defaultStoryParameters, - hugin3DBounds, hugin25mDepthMapLayer, hugin25mKhNetmapMapLayer, hugin25mKhNetmapMapLayerPng, + hugin3DBounds, + huginAxes3DLayer, + mainStyle, northArrowLayer, volveWellsBounds, volveWellsLayer, volveWellsWithLogsLayer, } from "../sharedSettings"; +import { GeoJsonLayer } from "@deck.gl/layers/typed"; const stories: Meta = { component: SubsurfaceViewer, @@ -55,6 +57,22 @@ const Root = styled("div")({ }, }); +const SQUARE = { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [ + [ + [5, 5], + [5, 1500], + [1500, 1500], + [1500, 5], + [5, 5], + ], + ], + }, +}; + const DisplayCameraPositionComponent: React.FC = ( args ) => { @@ -506,3 +524,48 @@ export const AddLayer: StoryObj = { }, render: (args) => , }; + +const ScaleYComponent = ({ verticalScale }: { verticalScale: number }) => { + const viewerProps: SubsurfaceViewerProps = { + id: "ScaleY", + bounds: [-1000, -1000, 3000, 3000], + layers: [ + new Axes2DLayer({ + id: "axes", + }), + new GeoJsonLayer({ + ...customLayerWithPolygonDataProps, + getLineColor: [0, 0, 0], + data: SQUARE, + }), + ], + views: { + layout: [1, 1], + viewports: [ + { + id: "section", + verticalScale, + }, + ], + }, + }; + return ; +}; + +export const ScaleY: StoryObj = { + args: { verticalScale: 1.5 }, + argTypes: { + verticalScale: { + control: { type: "range", min: -1, max: 10, step: 0.1 }, + }, + }, + parameters: { + docs: { + ...defaultStoryParameters.docs, + description: { + story: "Vertical scaling example in Orthographic view.", + }, + }, + }, + render: (args) => , +}; diff --git a/typescript/packages/subsurface-viewer/src/storybook/examples/__image_snapshots__/subsurfaceviewer-examples-camera--scale-y.png b/typescript/packages/subsurface-viewer/src/storybook/examples/__image_snapshots__/subsurfaceviewer-examples-camera--scale-y.png new file mode 100644 index 000000000..7b02c7f1d Binary files /dev/null and b/typescript/packages/subsurface-viewer/src/storybook/examples/__image_snapshots__/subsurfaceviewer-examples-camera--scale-y.png differ diff --git a/typescript/packages/subsurface-viewer/src/storybook/sharedSettings.tsx b/typescript/packages/subsurface-viewer/src/storybook/sharedSettings.tsx index 55026bf1b..14d933478 100644 --- a/typescript/packages/subsurface-viewer/src/storybook/sharedSettings.tsx +++ b/typescript/packages/subsurface-viewer/src/storybook/sharedSettings.tsx @@ -8,6 +8,7 @@ import SubsurfaceViewer from "../SubsurfaceViewer"; import type { BoundingBox3D } from "../utils/BoundingBox3D"; import exampleData from "../../../../../example-data/deckgl-map.json"; +import type { GeoJsonLayerProps } from "@deck.gl/layers/typed"; export const defaultStoryParameters = { docs: { @@ -216,10 +217,8 @@ export const customLayerWithPolylineData = { }; // Data for custom geojson layer with polygon data -export const customLayerWithPolygonData = { - "@@type": "GeoJsonLayer", +export const customLayerWithPolygonDataProps: GeoJsonLayerProps = { id: "geojson-layer", - name: "Polygon", data: { type: "Feature", properties: {}, @@ -243,6 +242,11 @@ export const customLayerWithPolygonData = { opacity: 0.3, }; +export const customLayerWithPolygonData = { + ...customLayerWithPolygonDataProps, + "@@type": "GeoJsonLayer", +}; + // Data for custom text layer export const customLayerWithTextData = { "@@type": "TextLayer", diff --git a/typescript/packages/subsurface-viewer/src/utils/camera.test.ts b/typescript/packages/subsurface-viewer/src/utils/camera.test.ts new file mode 100644 index 000000000..8582517c7 --- /dev/null +++ b/typescript/packages/subsurface-viewer/src/utils/camera.test.ts @@ -0,0 +1,69 @@ +import "jest"; + +import { getZoom, useLateralZoom } from "./camera"; +import type { ViewStateType, ViewportType } from "../components/Map"; +import { renderHook } from "@testing-library/react"; + +describe("Test zoom", () => { + const defaultZoom = 3; + + it("Test default zoom", () => { + const viewport: ViewportType = { id: "" }; + const zoom = getZoom(viewport, defaultZoom); + expect(zoom).toEqual(defaultZoom); + }); + + it("Test zoom override", () => { + const viewport: ViewportType = { id: "", zoom: 10 }; + const zoom = getZoom(viewport, defaultZoom); + expect(zoom).toEqual(viewport.zoom); + }); + + it("Test vertical scale", () => { + const viewport: ViewportType = { id: "", zoom: 10, verticalScale: 4 }; + const zoom = getZoom(viewport, defaultZoom); + expect(zoom).toEqual([viewport.zoom, 5]); + }); + + it("Test zero scale", () => { + const viewport: ViewportType = { id: "", zoom: 10, verticalScale: 0 }; + const zoom = getZoom(viewport, defaultZoom); + expect(zoom).toEqual(viewport.zoom); + }); + + it("Test negative scale", () => { + const viewport: ViewportType = { id: "", zoom: 10, verticalScale: -1 }; + const zoom = getZoom(viewport, defaultZoom); + expect(zoom).toEqual([viewport.zoom, 10]); + }); +}); + +describe("Test lateral zoom hook", () => { + it("Test no zoom", () => { + const viewState: Record = {}; + + const { result } = renderHook(() => useLateralZoom(viewState)); + + expect(result.current).toEqual(-5); + }); + + it("Test zero zoom", () => { + const viewState: Record = { + a: { zoom: 0, target: [0, 0], rotationX: 0, rotationOrbit: 0 }, + }; + + const { result } = renderHook(() => useLateralZoom(viewState)); + + expect(result.current).toEqual(viewState["a"].zoom); + }); + + it("Test scaled zoom", () => { + const viewState: Record = { + a: { zoom: [5, 6], target: [0, 0], rotationX: 0, rotationOrbit: 0 }, + }; + + const { result } = renderHook(() => useLateralZoom(viewState)); + + expect(result.current).toEqual(5); + }); +}); diff --git a/typescript/packages/subsurface-viewer/src/utils/camera.ts b/typescript/packages/subsurface-viewer/src/utils/camera.ts new file mode 100644 index 000000000..d4040a014 --- /dev/null +++ b/typescript/packages/subsurface-viewer/src/utils/camera.ts @@ -0,0 +1,25 @@ +import React from "react"; +import type { ViewStateType, ViewportType } from "../components/Map"; +import _ from "lodash"; + +export const getZoom = (viewport: ViewportType, fb_zoom: number) => { + const zoom = viewport.zoom ?? fb_zoom; + const scaledZoom: [number, number] = [ + zoom, + zoom / Math.sqrt(Math.max(viewport.verticalScale || 0, 0) || 1), + ]; + + return viewport.verticalScale ? scaledZoom : zoom; +}; + +export const useLateralZoom = (viewState: Record) => { + return React.useMemo(() => { + const zoom = _.find(viewState)?.zoom; + + if (_.isArray(zoom)) { + return zoom[0]; + } else { + return zoom ?? -5; + } + }, [viewState]); +};