From 30beb3bd2695ff863db5002fa45edcc80954bc1f Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Tue, 23 Jul 2024 15:09:13 -0700 Subject: [PATCH 01/12] feat: Added CameraTransform to ViewerState, hoisted view3d ref up to ShareModal --- src/aics-image-viewer/components/App/index.tsx | 14 +++++++++++++- src/aics-image-viewer/components/App/types.ts | 4 +++- .../components/ViewerStateProvider/index.tsx | 1 + .../components/ViewerStateProvider/types.ts | 3 ++- website/components/AppWrapper.tsx | 11 +++++++++-- website/components/Modals/ShareModal.tsx | 9 ++++++++- 6 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/aics-image-viewer/components/App/index.tsx b/src/aics-image-viewer/components/App/index.tsx index e089c055..130a8ce5 100644 --- a/src/aics-image-viewer/components/App/index.tsx +++ b/src/aics-image-viewer/components/App/index.tsx @@ -123,6 +123,7 @@ const defaultViewerSettings: ViewerState = { region: { x: [0, 1], y: [0, 1], z: [0, 1] }, slice: { x: 0.5, y: 0.5, z: 0.5 }, time: 0, + cameraTransform: undefined, }; const DEFAULT_CHANNEL_STATE: ChannelState = { @@ -156,6 +157,7 @@ const defaultProps: AppProps = { parentImageDownloadHref: "", pixelSize: undefined, canvasMargin: "0 0 0 0", + view3dRef: undefined, }; const axisToLoaderPriority: Record = { @@ -227,6 +229,9 @@ const App: React.FC = (props) => { viewerState.current; const view3d = useConstructor(() => new View3d()); + if (props.view3dRef !== undefined) { + props.view3dRef.current = view3d; + } const loadContext = useConstructor( () => new VolumeLoaderContext(CACHE_MAX_SIZE, QUEUE_MAX_SIZE, QUEUE_MAX_LOW_PRIORITY_SIZE) ); @@ -239,6 +244,7 @@ const App: React.FC = (props) => { _showError(error); setSendingQueryRequest(false); }; + useEffect(() => { // Get notifications of loading errors which occur after the initial load, e.g. on time change or new channel load view3d.setLoadErrorHandler((_vol, e) => showError(e)); @@ -585,7 +591,6 @@ const App: React.FC = (props) => { }; // Effects to imperatively sync `viewerSettings` to `view3d` - useImageEffect( (_currentImage) => { view3d.setCameraMode(viewerSettings.viewMode); @@ -594,6 +599,13 @@ const App: React.FC = (props) => { [viewerSettings.viewMode] ); + useImageEffect((_currentImage) => { + // Set camera transform on initial load only + if (viewerSettings.cameraTransform) { + view3d.setCameraTransform(viewerSettings.cameraTransform); + } + }, []); + useImageEffect((_currentImage) => view3d.setAutoRotate(viewerSettings.autorotate), [viewerSettings.autorotate]); useImageEffect((_currentImage) => view3d.setShowAxis(viewerSettings.showAxes), [viewerSettings.showAxes]); diff --git a/src/aics-image-viewer/components/App/types.ts b/src/aics-image-viewer/components/App/types.ts index 0ac6e327..56f9894c 100644 --- a/src/aics-image-viewer/components/App/types.ts +++ b/src/aics-image-viewer/components/App/types.ts @@ -1,8 +1,9 @@ -import type { RawArrayData, RawArrayInfo, Volume } from "@aics/volume-viewer"; +import type { RawArrayData, RawArrayInfo, View3d, Volume } from "@aics/volume-viewer"; import type { MetadataRecord } from "../../shared/types"; import type { ViewerChannelSettings } from "../../shared/utils/viewerChannelSettings"; import type { ViewerState } from "../ViewerStateProvider/types"; +import { MutableRefObject } from "react"; /** `typeof useEffect`, but the effect handler takes a `Volume` as an argument */ export type UseImageEffectType = (effect: (image: Volume) => void | (() => void), deps: React.DependencyList) => void; @@ -55,6 +56,7 @@ export interface AppProps { }; metadata?: MetadataRecord; + view3dRef?: MutableRefObject; metadataFormatter?: (metadata: MetadataRecord) => MetadataRecord; onControlPanelToggle?: (collapsed: boolean) => void; } diff --git a/src/aics-image-viewer/components/ViewerStateProvider/index.tsx b/src/aics-image-viewer/components/ViewerStateProvider/index.tsx index 79282fc1..f946e50d 100644 --- a/src/aics-image-viewer/components/ViewerStateProvider/index.tsx +++ b/src/aics-image-viewer/components/ViewerStateProvider/index.tsx @@ -41,6 +41,7 @@ const DEFAULT_VIEWER_SETTINGS: ViewerState = { region: { x: [0, 1], y: [0, 1], z: [0, 1] }, slice: { x: 0.5, y: 0.5, z: 0.5 }, time: 0, + cameraTransform: undefined, }; // Some viewer settings require custom change behaviors to change related settings simultaneously or guard against diff --git a/src/aics-image-viewer/components/ViewerStateProvider/types.ts b/src/aics-image-viewer/components/ViewerStateProvider/types.ts index 935bd005..d21a0f0a 100644 --- a/src/aics-image-viewer/components/ViewerStateProvider/types.ts +++ b/src/aics-image-viewer/components/ViewerStateProvider/types.ts @@ -1,4 +1,4 @@ -import { ControlPoint } from "@aics/volume-viewer"; +import { CameraTransform, ControlPoint } from "@aics/volume-viewer"; import type { ImageType, RenderMode, ViewMode } from "../../shared/enums"; import type { PerAxis } from "../../shared/types"; import type { ColorArray } from "../../shared/utils/colorRepresentations"; @@ -26,6 +26,7 @@ export interface ViewerState { // This state is active in x,y,z single slice modes. slice: PerAxis; time: number; + cameraTransform: Partial | undefined; } export type ViewerStateKey = keyof ViewerState; diff --git a/website/components/AppWrapper.tsx b/website/components/AppWrapper.tsx index f5e9c814..b809720c 100644 --- a/website/components/AppWrapper.tsx +++ b/website/components/AppWrapper.tsx @@ -10,6 +10,7 @@ import { ImageViewerApp, ViewerStateProvider } from "../../src"; import { ViewerState } from "../../src/aics-image-viewer/components/ViewerStateProvider/types"; import { AppDataProps } from "../types"; import { parseViewerUrlParams } from "../utils/url_utils"; +import { View3d } from "@aics/volume-viewer"; const DEFAULT_APP_PROPS: AppDataProps = { imageUrl: "", @@ -37,6 +38,7 @@ export default function AppWrapper(): ReactElement { const location = useLocation(); const navigation = useNavigate(); + const view3dRef = React.useRef(null); const [viewerSettings, setViewerSettings] = useState>({}); const [viewerProps, setViewerProps] = useState(null); const [searchParams] = useSearchParams(); @@ -94,13 +96,18 @@ export default function AppWrapper(): ReactElement { - {viewerProps && } + {viewerProps && } {viewerProps && ( - + )} diff --git a/website/components/Modals/ShareModal.tsx b/website/components/Modals/ShareModal.tsx index 5ec84508..057223a2 100644 --- a/website/components/Modals/ShareModal.tsx +++ b/website/components/Modals/ShareModal.tsx @@ -11,9 +11,12 @@ import { } from "../../../src/aics-image-viewer/components/ViewerStateProvider"; import { ViewerStateContextType } from "../../../src/aics-image-viewer/components/ViewerStateProvider/types"; import { serializeViewerUrlParams } from "../../utils/url_utils"; +import { View3d } from "@aics/volume-viewer"; type ShareModalProps = { appProps: AppDataProps; + // Used to retrieve the current camera position information + view3dRef?: React.RefObject; } & ViewerStateContextType; const ModalContainer = styled.div``; @@ -30,7 +33,11 @@ const ShareModal: React.FC = (props: ShareModalProps) => { // location.pathname will include up to `.../viewer` const baseUrl = location.protocol + "//" + location.host + location.pathname; - let serializedViewerParams = serializeViewerUrlParams(props) as Record; + const paramProps = { + ...props, + cameraTransform: props.view3dRef?.current?.getCameraTransform(), + }; + let serializedViewerParams = serializeViewerUrlParams(paramProps) as Record; if (props.appProps.imageUrl) { let serializedUrl; From d68fdc2449011585f7232ff3b7c6c4f482bdb9bb Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Tue, 23 Jul 2024 15:24:06 -0700 Subject: [PATCH 02/12] feat: Saving and loading camera transforms from the URL --- website/utils/url_utils.ts | 51 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/website/utils/url_utils.ts b/website/utils/url_utils.ts index c4ddac60..f672dd29 100644 --- a/website/utils/url_utils.ts +++ b/website/utils/url_utils.ts @@ -14,6 +14,7 @@ import { import { ColorArray } from "../../src/aics-image-viewer/shared/utils/colorRepresentations"; import { PerAxis } from "../../src/aics-image-viewer/shared/types"; import { clamp } from "./math_utils"; +import { CameraTransform } from "@aics/volume-viewer"; const CHANNEL_STATE_KEY_REGEX = /^c[0-9]+$/; /** Match colon-separated pairs of alphanumeric strings */ @@ -50,6 +51,14 @@ export enum ViewerStateKeys { Region = "reg", Slice = "slice", Time = "t", + CameraTransform = "cam", +} + +export enum CameraTransformKeys { + Position = "pos", + Target = "tar", + Up = "up", + Rotation = "rot", } /** @@ -156,6 +165,17 @@ export class ViewerStateParams { [ViewerStateKeys.Slice]?: string = undefined; /** Frame number, for time-series volumes. 0 by default. */ [ViewerStateKeys.Time]?: string = undefined; + /** + * Camera transform settings, as a list of `key:value` pairs separated by commas. + * Valid keys are defined in `CameraTransformKeys`: + * - `pos`: position + * - `tar`: target + * - `up`: up + * - `rot`: rotation + * + * All values are an array of three floats, separated by commas. + */ + [ViewerStateKeys.CameraTransform]?: string = undefined; } /** URL parameters that define data sources when loading volumes. */ @@ -375,7 +395,7 @@ function parseStringSlice(region: string | undefined): PerAxis | undefin return { x, y, z }; } -function parseStringLevels(levels: string | undefined): [number, number, number] | undefined { +function parseThreeNumberArray(levels: string | undefined): [number, number, number] | undefined { if (!levels) { return undefined; } @@ -406,6 +426,29 @@ function parseStringRegion(region: string | undefined): PerAxis<[number, number] return { x, y, z }; } +function parseCameraTransform(cameraSettings: string | undefined): Partial | undefined { + if (!cameraSettings) { + return undefined; + } + const parsedCameraSettings = parseKeyValueList(cameraSettings); + const result: Partial = { + position: parseThreeNumberArray(parsedCameraSettings[CameraTransformKeys.Position]), + target: parseThreeNumberArray(parsedCameraSettings[CameraTransformKeys.Target]), + up: parseThreeNumberArray(parsedCameraSettings[CameraTransformKeys.Up]), + rotation: parseThreeNumberArray(parsedCameraSettings[CameraTransformKeys.Rotation]), + }; + return removeUndefinedProperties(result); +} + +function serializeCameraTransform(cameraTransform: CameraTransform): string { + return objectToKeyValueList({ + [CameraTransformKeys.Position]: cameraTransform.position.join(","), + [CameraTransformKeys.Target]: cameraTransform.target.join(","), + [CameraTransformKeys.Up]: cameraTransform.up.join(","), + [CameraTransformKeys.Rotation]: cameraTransform.rotation.join(","), + }); +} + //// DATA SERIALIZATION ////////////////////// /** @@ -464,12 +507,13 @@ export function deserializeViewerState(params: ViewerStateParams): Partial): ViewerStatePa [ViewerStateKeys.Slice]: state.slice && `${state.slice.x},${state.slice.y},${state.slice.z}`, [ViewerStateKeys.Levels]: state.levels?.join(","), [ViewerStateKeys.Time]: state.time?.toString(), + // All CameraTransform properties will be provided when serializing viewer state + [ViewerStateKeys.CameraTransform]: + state.cameraTransform && serializeCameraTransform(state.cameraTransform as CameraTransform), }; const viewModeToViewParam = { [ViewMode.threeD]: "3D", From 5b5401940fbdeb82141dc5a12d4a5a79919cdec3 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Tue, 23 Jul 2024 15:35:18 -0700 Subject: [PATCH 03/12] fix: Fixed some unit tests --- website/utils/test/url_utils.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/website/utils/test/url_utils.test.ts b/website/utils/test/url_utils.test.ts index a0a13940..4eaa57e8 100644 --- a/website/utils/test/url_utils.test.ts +++ b/website/utils/test/url_utils.test.ts @@ -376,6 +376,7 @@ describe("Viewer state serialization", () => { region: { x: [0, 1], y: [0, 1], z: [0, 1] }, slice: { x: 0.5, y: 0.5, z: 0.5 }, time: 0, + cameraTransform: undefined, }; const SERIALIZED_DEFAULT_VIEWER_STATE: ViewerStateParams = { mode: "volumetric", @@ -413,6 +414,12 @@ describe("Viewer state serialization", () => { region: { x: [0, 0.5], y: [0, 1], z: [0, 1] }, slice: { x: 0.25, y: 0.75, z: 0.5 }, time: 100, + cameraTransform: { + position: [-1, -4, 45], + target: [0, 0, 0], + rotation: [-56, 14, 6], + up: [0, 1, 0], + }, }; const SERIALIZED_CUSTOM_VIEWER_STATE: ViewerStateParams = { mode: "pathtrace", @@ -431,6 +438,7 @@ describe("Viewer state serialization", () => { reg: "0:0.5,0:1,0:1", slice: "0.25,0.75,0.5", t: "100", + cam: "pos:-1%2C-4%2C45,tar:0%2C0%2C0,up:0%2C1%2C0,rot:-56%2C14%2C6", }; describe("serializeViewerState", () => { From 689f13f9c0f51b3c7a80515e97b396b978d249ae Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Tue, 23 Jul 2024 15:39:24 -0700 Subject: [PATCH 04/12] fix: Fixed parsing negative numbers --- website/utils/url_utils.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/website/utils/url_utils.ts b/website/utils/url_utils.ts index 95a4e3c3..f84c6dac 100644 --- a/website/utils/url_utils.ts +++ b/website/utils/url_utils.ts @@ -428,11 +428,15 @@ function parseStringSlice(region: string | undefined): PerAxis | undefin return { x, y, z }; } -function parseThreeNumberArray(levels: string | undefined): [number, number, number] | undefined { +function parseThreeNumberArray( + levels: string | undefined, + min?: number, + max?: number +): [number, number, number] | undefined { if (!levels) { return undefined; } - const [low, middle, high] = levels.split(",").map((val) => parseStringFloat(val, 0, 255)); + const [low, middle, high] = levels.split(",").map((val) => parseStringFloat(val, min ?? -Infinity, max ?? Infinity)); if (low === undefined || middle === undefined || high === undefined) { return undefined; } @@ -571,7 +575,7 @@ export function deserializeViewerState(params: ViewerStateParams): Partial Date: Wed, 24 Jul 2024 10:54:29 -0700 Subject: [PATCH 05/12] refactor: Code and comment cleanup in url_utils --- website/utils/url_utils.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/website/utils/url_utils.ts b/website/utils/url_utils.ts index f84c6dac..fc61090b 100644 --- a/website/utils/url_utils.ts +++ b/website/utils/url_utils.ts @@ -206,7 +206,8 @@ export class ViewerStateParams { * - `up`: up * - `rot`: rotation * - * All values are an array of three floats, separated by commas. + * All values are an array of three floats, separated by commas and + * encoded using `encodeURIComponent`. */ [ViewerStateKeys.CameraTransform]?: string = undefined; } @@ -428,15 +429,18 @@ function parseStringSlice(region: string | undefined): PerAxis | undefin return { x, y, z }; } +/** + * Parses a Vector3-like array of three numbers from a string. + */ function parseThreeNumberArray( levels: string | undefined, - min?: number, - max?: number + min: number = -Infinity, + max: number = Infinity ): [number, number, number] | undefined { if (!levels) { return undefined; } - const [low, middle, high] = levels.split(",").map((val) => parseStringFloat(val, min ?? -Infinity, max ?? Infinity)); + const [low, middle, high] = levels.split(",").map((val) => parseStringFloat(val, min, max)); if (low === undefined || middle === undefined || high === undefined) { return undefined; } From efd724694d734843fa0aa2351666f8617040b718 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Wed, 24 Jul 2024 14:49:48 -0700 Subject: [PATCH 06/12] fix: Added missing orthographic camera scale to CameraTransform --- website/utils/url_utils.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/website/utils/url_utils.ts b/website/utils/url_utils.ts index fc61090b..36b68571 100644 --- a/website/utils/url_utils.ts +++ b/website/utils/url_utils.ts @@ -62,10 +62,16 @@ export enum ViewerStateKeys { } export enum CameraTransformKeys { + /** Camera position in 3D coordinates. */ Position = "pos", + /** Target position of the trackball controls in 3D coordinates. */ Target = "tar", + /** The up vector of the camera. Will be normalized to magnitude of 1. */ Up = "up", + /** Euler rotation in radians. */ Rotation = "rot", + /** Scale factor for the X, Y, and Z orthographic cameras. */ + OrthoScales = "ort", } /** @@ -205,6 +211,7 @@ export class ViewerStateParams { * - `tar`: target * - `up`: up * - `rot`: rotation + * - `ort`: orthographic scales * * All values are an array of three floats, separated by commas and * encoded using `encodeURIComponent`. @@ -477,6 +484,7 @@ function parseCameraTransform(cameraSettings: string | undefined): Partial Date: Wed, 24 Jul 2024 16:43:59 -0700 Subject: [PATCH 07/12] refactor: Comment cleanup, enforced non-negative ortho scales --- website/utils/url_utils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/website/utils/url_utils.ts b/website/utils/url_utils.ts index 36b68571..c4e64bc4 100644 --- a/website/utils/url_utils.ts +++ b/website/utils/url_utils.ts @@ -437,7 +437,7 @@ function parseStringSlice(region: string | undefined): PerAxis | undefin } /** - * Parses a Vector3-like array of three numbers from a string. + * Parses an array of three numbers from a string. */ function parseThreeNumberArray( levels: string | undefined, @@ -484,7 +484,8 @@ function parseCameraTransform(cameraSettings: string | undefined): Partial Date: Wed, 24 Jul 2024 16:44:07 -0700 Subject: [PATCH 08/12] fix: Added ortho scales to unit tests --- website/utils/test/url_utils.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/website/utils/test/url_utils.test.ts b/website/utils/test/url_utils.test.ts index 4eaa57e8..1401d767 100644 --- a/website/utils/test/url_utils.test.ts +++ b/website/utils/test/url_utils.test.ts @@ -415,10 +415,11 @@ describe("Viewer state serialization", () => { slice: { x: 0.25, y: 0.75, z: 0.5 }, time: 100, cameraTransform: { - position: [-1, -4, 45], + position: [-1.05, -4, 45], target: [0, 0, 0], rotation: [-56, 14, 6], up: [0, 1, 0], + orthoScales: [0.5, 0.002, 3.498], }, }; const SERIALIZED_CUSTOM_VIEWER_STATE: ViewerStateParams = { @@ -438,7 +439,7 @@ describe("Viewer state serialization", () => { reg: "0:0.5,0:1,0:1", slice: "0.25,0.75,0.5", t: "100", - cam: "pos:-1%2C-4%2C45,tar:0%2C0%2C0,up:0%2C1%2C0,rot:-56%2C14%2C6", + cam: "pos:-1.05%2C-4%2C45,tar:0%2C0%2C0,up:0%2C1%2C0,rot:-56%2C14%2C6,ort:0.5%2C0.002%2C3.498", }; describe("serializeViewerState", () => { From b5c9837f4fce1b1c2fd38a79dca9b39738ab89d2 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Fri, 2 Aug 2024 16:20:43 -0700 Subject: [PATCH 09/12] refactor: Updated CameraTransform to CameraState, handlers for FOV and orthoScale --- .../components/App/index.tsx | 6 +-- .../components/ViewerStateProvider/types.ts | 4 +- website/components/Modals/ShareModal.tsx | 3 +- website/utils/test/url_utils.test.ts | 10 ++-- website/utils/url_utils.ts | 49 ++++++++++--------- 5 files changed, 39 insertions(+), 33 deletions(-) diff --git a/src/aics-image-viewer/components/App/index.tsx b/src/aics-image-viewer/components/App/index.tsx index b57f8f6d..8f26f6f8 100644 --- a/src/aics-image-viewer/components/App/index.tsx +++ b/src/aics-image-viewer/components/App/index.tsx @@ -123,7 +123,7 @@ const defaultViewerSettings: ViewerState = { region: { x: [0, 1], y: [0, 1], z: [0, 1] }, slice: { x: 0.5, y: 0.5, z: 0.5 }, time: 0, - cameraTransform: undefined, + cameraState: undefined, }; const DEFAULT_CHANNEL_STATE: ChannelState = { @@ -600,8 +600,8 @@ const App: React.FC = (props) => { useImageEffect((_currentImage) => { // Set camera transform on initial load only - if (viewerSettings.cameraTransform) { - view3d.setCameraTransform(viewerSettings.cameraTransform); + if (viewerSettings.cameraState) { + view3d.setCameraState(viewerSettings.cameraState); } }, []); diff --git a/src/aics-image-viewer/components/ViewerStateProvider/types.ts b/src/aics-image-viewer/components/ViewerStateProvider/types.ts index d21a0f0a..c8bfadb3 100644 --- a/src/aics-image-viewer/components/ViewerStateProvider/types.ts +++ b/src/aics-image-viewer/components/ViewerStateProvider/types.ts @@ -1,4 +1,4 @@ -import { CameraTransform, ControlPoint } from "@aics/volume-viewer"; +import { CameraState, ControlPoint } from "@aics/volume-viewer"; import type { ImageType, RenderMode, ViewMode } from "../../shared/enums"; import type { PerAxis } from "../../shared/types"; import type { ColorArray } from "../../shared/utils/colorRepresentations"; @@ -26,7 +26,7 @@ export interface ViewerState { // This state is active in x,y,z single slice modes. slice: PerAxis; time: number; - cameraTransform: Partial | undefined; + cameraState: Partial | undefined; } export type ViewerStateKey = keyof ViewerState; diff --git a/website/components/Modals/ShareModal.tsx b/website/components/Modals/ShareModal.tsx index 057223a2..8f9bf17a 100644 --- a/website/components/Modals/ShareModal.tsx +++ b/website/components/Modals/ShareModal.tsx @@ -35,8 +35,9 @@ const ShareModal: React.FC = (props: ShareModalProps) => { const baseUrl = location.protocol + "//" + location.host + location.pathname; const paramProps = { ...props, - cameraTransform: props.view3dRef?.current?.getCameraTransform(), + cameraState: props.view3dRef?.current?.getCameraState(), }; + console.log(paramProps.cameraState); let serializedViewerParams = serializeViewerUrlParams(paramProps) as Record; if (props.appProps.imageUrl) { diff --git a/website/utils/test/url_utils.test.ts b/website/utils/test/url_utils.test.ts index 1401d767..b0447171 100644 --- a/website/utils/test/url_utils.test.ts +++ b/website/utils/test/url_utils.test.ts @@ -376,7 +376,7 @@ describe("Viewer state serialization", () => { region: { x: [0, 1], y: [0, 1], z: [0, 1] }, slice: { x: 0.5, y: 0.5, z: 0.5 }, time: 0, - cameraTransform: undefined, + cameraState: undefined, }; const SERIALIZED_DEFAULT_VIEWER_STATE: ViewerStateParams = { mode: "volumetric", @@ -414,12 +414,12 @@ describe("Viewer state serialization", () => { region: { x: [0, 0.5], y: [0, 1], z: [0, 1] }, slice: { x: 0.25, y: 0.75, z: 0.5 }, time: 100, - cameraTransform: { + cameraState: { position: [-1.05, -4, 45], target: [0, 0, 0], - rotation: [-56, 14, 6], up: [0, 1, 0], - orthoScales: [0.5, 0.002, 3.498], + orthoScale: 3.534, + fov: 43.5, }, }; const SERIALIZED_CUSTOM_VIEWER_STATE: ViewerStateParams = { @@ -439,7 +439,7 @@ describe("Viewer state serialization", () => { reg: "0:0.5,0:1,0:1", slice: "0.25,0.75,0.5", t: "100", - cam: "pos:-1.05%2C-4%2C45,tar:0%2C0%2C0,up:0%2C1%2C0,rot:-56%2C14%2C6,ort:0.5%2C0.002%2C3.498", + cam: "pos:-1.05%2C-4%2C45,tar:0%2C0%2C0,up:0%2C1%2C0,ort:3.534,fov:43.5", }; describe("serializeViewerState", () => { diff --git a/website/utils/url_utils.ts b/website/utils/url_utils.ts index c4e64bc4..b7577e76 100644 --- a/website/utils/url_utils.ts +++ b/website/utils/url_utils.ts @@ -14,7 +14,7 @@ import { import { ColorArray } from "../../src/aics-image-viewer/shared/utils/colorRepresentations"; import { PerAxis } from "../../src/aics-image-viewer/shared/types"; import { clamp } from "./math_utils"; -import { CameraTransform, ControlPoint } from "@aics/volume-viewer"; +import { CameraState, ControlPoint } from "@aics/volume-viewer"; const CHANNEL_STATE_KEY_REGEX = /^c[0-9]+$/; /** Match colon-separated pairs of alphanumeric strings */ @@ -58,7 +58,7 @@ export enum ViewerStateKeys { Region = "reg", Slice = "slice", Time = "t", - CameraTransform = "cam", + CameraState = "cam", } export enum CameraTransformKeys { @@ -68,10 +68,10 @@ export enum CameraTransformKeys { Target = "tar", /** The up vector of the camera. Will be normalized to magnitude of 1. */ Up = "up", - /** Euler rotation in radians. */ - Rotation = "rot", - /** Scale factor for the X, Y, and Z orthographic cameras. */ - OrthoScales = "ort", + /** Scale factor for orthographic cameras. */ + OrthoScale = "ort", + /** Vertical FOV of the camera view frustum, from top to bottom, in degrees. */ + Fov = "fov", } /** @@ -216,7 +216,7 @@ export class ViewerStateParams { * All values are an array of three floats, separated by commas and * encoded using `encodeURIComponent`. */ - [ViewerStateKeys.CameraTransform]?: string = undefined; + [ViewerStateKeys.CameraState]?: string = undefined; } /** URL parameters that define data sources when loading volumes. */ @@ -323,10 +323,14 @@ export function parseKeyValueList(data: string): Record { return result; } -export function objectToKeyValueList(obj: Record): string { +export function objectToKeyValueList(obj: Record): string { const keyValuePairs: string[] = []; for (const key in obj) { - keyValuePairs.push(`${encodeURIComponent(key)}:${encodeURIComponent(obj[key].trim())}`); + const value = obj[key]; + if (value === undefined) { + continue; + } + keyValuePairs.push(`${encodeURIComponent(key)}:${encodeURIComponent(value.trim())}`); } return keyValuePairs.join(","); } @@ -474,29 +478,29 @@ function parseStringRegion(region: string | undefined): PerAxis<[number, number] return { x, y, z }; } -function parseCameraTransform(cameraSettings: string | undefined): Partial | undefined { +function parseCameraState(cameraSettings: string | undefined): Partial | undefined { if (!cameraSettings) { return undefined; } const parsedCameraSettings = parseKeyValueList(cameraSettings); - const result: Partial = { + const result: Partial = { position: parseThreeNumberArray(parsedCameraSettings[CameraTransformKeys.Position]), target: parseThreeNumberArray(parsedCameraSettings[CameraTransformKeys.Target]), up: parseThreeNumberArray(parsedCameraSettings[CameraTransformKeys.Up]), - rotation: parseThreeNumberArray(parsedCameraSettings[CameraTransformKeys.Rotation]), // Orthographic scales cannot be negative - orthoScales: parseThreeNumberArray(parsedCameraSettings[CameraTransformKeys.OrthoScales], 0, Infinity), + orthoScale: parseStringFloat(parsedCameraSettings[CameraTransformKeys.OrthoScale], 0, Infinity), + fov: parseStringFloat(parsedCameraSettings[CameraTransformKeys.Fov], 0, 180), }; return removeUndefinedProperties(result); } -function serializeCameraTransform(cameraTransform: CameraTransform): string { +function serializeCameraState(cameraState: CameraState): string { return objectToKeyValueList({ - [CameraTransformKeys.Position]: cameraTransform.position.join(","), - [CameraTransformKeys.Target]: cameraTransform.target.join(","), - [CameraTransformKeys.Up]: cameraTransform.up.join(","), - [CameraTransformKeys.Rotation]: cameraTransform.rotation.join(","), - [CameraTransformKeys.OrthoScales]: cameraTransform.orthoScales.join(","), + [CameraTransformKeys.Position]: cameraState.position.join(","), + [CameraTransformKeys.Target]: cameraState.target.join(","), + [CameraTransformKeys.Up]: cameraState.up.join(","), + [CameraTransformKeys.OrthoScale]: cameraState.orthoScale?.toString(), + [CameraTransformKeys.Fov]: cameraState.fov?.toString(), }); } @@ -595,7 +599,7 @@ export function deserializeViewerState(params: ViewerStateParams): Partial): ViewerStatePa [ViewerStateKeys.Levels]: state.levels?.join(","), [ViewerStateKeys.Time]: state.time?.toString(), // All CameraTransform properties will be provided when serializing viewer state - [ViewerStateKeys.CameraTransform]: - state.cameraTransform && serializeCameraTransform(state.cameraTransform as CameraTransform), + [ViewerStateKeys.CameraState]: state.cameraState && serializeCameraState(state.cameraState as CameraState), }; const viewModeToViewParam = { [ViewMode.threeD]: "3D", @@ -813,6 +816,8 @@ export async function parseViewerUrlParams(urlSearchParams: URLSearchParams): Pr args = { ...args, ...datasetArgs }; } + console.log("args", args); + console.log("viewer settings", viewerSettings); return { args: removeUndefinedProperties(args), viewerSettings: removeUndefinedProperties(viewerSettings) }; } From 7bfea70fca7d1d017683ce965183d6c54ae03aa6 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Mon, 5 Aug 2024 12:15:00 -0700 Subject: [PATCH 10/12] refactor: Removed console statement --- website/components/Modals/ShareModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/website/components/Modals/ShareModal.tsx b/website/components/Modals/ShareModal.tsx index 8f9bf17a..332834ba 100644 --- a/website/components/Modals/ShareModal.tsx +++ b/website/components/Modals/ShareModal.tsx @@ -37,7 +37,6 @@ const ShareModal: React.FC = (props: ShareModalProps) => { ...props, cameraState: props.view3dRef?.current?.getCameraState(), }; - console.log(paramProps.cameraState); let serializedViewerParams = serializeViewerUrlParams(paramProps) as Record; if (props.appProps.imageUrl) { From 9930846aa0382a5c4fba228d9235a42204f699f9 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 8 Aug 2024 16:12:37 -0700 Subject: [PATCH 11/12] refactor: Updated to `volume-viewer` v3.11.0 --- package-lock.json | 8 ++++---- package.json | 2 +- .../components/ViewerStateProvider/index.tsx | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 01654c32..c4b6bdd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "2.9.1", "license": "ISC", "dependencies": { - "@aics/volume-viewer": "^3.10.0", + "@aics/volume-viewer": "^3.11.0", "@ant-design/icons": "^5.2.5", "@fortawesome/fontawesome-svg-core": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.5.2", @@ -102,9 +102,9 @@ } }, "node_modules/@aics/volume-viewer": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/@aics/volume-viewer/-/volume-viewer-3.10.0.tgz", - "integrity": "sha512-8eaV1q5gOLpPgC9XkvWAeiYCzkad1Z9AH32C51NuiDKKPtL4SW2VolLMJaa0pgf2mhSpOiEYNih0C6lcXF7ciQ==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@aics/volume-viewer/-/volume-viewer-3.11.0.tgz", + "integrity": "sha512-09u1jGFbUeTw5sPUkbQ2NSSIA3FFIpTKaWS0da7ZdR5CsHBRiGjN1Vqgx3DTnhWPNrlylIkZsDOBCYCrvMzPaw==", "dependencies": { "geotiff": "^2.0.5", "serialize-error": "^11.0.3", diff --git a/package.json b/package.json index 27014076..cff3b51f 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "author": "Megan Riel-Mehan", "license": "ISC", "dependencies": { - "@aics/volume-viewer": "^3.10.0", + "@aics/volume-viewer": "^3.11.0", "@ant-design/icons": "^5.2.5", "@fortawesome/fontawesome-svg-core": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.5.2", diff --git a/src/aics-image-viewer/components/ViewerStateProvider/index.tsx b/src/aics-image-viewer/components/ViewerStateProvider/index.tsx index f946e50d..b97aa4ce 100644 --- a/src/aics-image-viewer/components/ViewerStateProvider/index.tsx +++ b/src/aics-image-viewer/components/ViewerStateProvider/index.tsx @@ -41,7 +41,7 @@ const DEFAULT_VIEWER_SETTINGS: ViewerState = { region: { x: [0, 1], y: [0, 1], z: [0, 1] }, slice: { x: 0.5, y: 0.5, z: 0.5 }, time: 0, - cameraTransform: undefined, + cameraState: undefined, }; // Some viewer settings require custom change behaviors to change related settings simultaneously or guard against From b4fbc0ade1d4069525b4d04c8bdcb6bb87aacbbb Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Fri, 9 Aug 2024 14:40:02 -0700 Subject: [PATCH 12/12] refactor: Sort imports, remove console logs, added test for deserializing partial camera states --- src/aics-image-viewer/components/App/types.ts | 2 +- website/utils/test/url_utils.test.ts | 12 ++++++++++++ website/utils/url_utils.ts | 2 -- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/aics-image-viewer/components/App/types.ts b/src/aics-image-viewer/components/App/types.ts index 56f9894c..e129e5a0 100644 --- a/src/aics-image-viewer/components/App/types.ts +++ b/src/aics-image-viewer/components/App/types.ts @@ -1,9 +1,9 @@ import type { RawArrayData, RawArrayInfo, View3d, Volume } from "@aics/volume-viewer"; +import { MutableRefObject } from "react"; import type { MetadataRecord } from "../../shared/types"; import type { ViewerChannelSettings } from "../../shared/utils/viewerChannelSettings"; import type { ViewerState } from "../ViewerStateProvider/types"; -import { MutableRefObject } from "react"; /** `typeof useEffect`, but the effect handler takes a `Volume` as an argument */ export type UseImageEffectType = (effect: (image: Volume) => void | (() => void), deps: React.DependencyList) => void; diff --git a/website/utils/test/url_utils.test.ts b/website/utils/test/url_utils.test.ts index b0447171..1528feaf 100644 --- a/website/utils/test/url_utils.test.ts +++ b/website/utils/test/url_utils.test.ts @@ -450,6 +450,18 @@ describe("Viewer state serialization", () => { it("serializes custom viewer settings", () => { expect(serializeViewerState(CUSTOM_VIEWER_STATE)).toEqual(SERIALIZED_CUSTOM_VIEWER_STATE); }); + + it("deserializes partial camera settings", () => { + const state: Partial = { + cameraState: { + position: [1.0, -1.4, 45], + up: [0, 1, 0], + fov: 43.5, + }, + }; + const serializedState = "pos:1%2C-1.4%2C45,up:0%2C1%2C0,fov:43.5"; + expect(deserializeViewerState({ cam: serializedState })).toEqual(state); + }); }); describe("deserializeViewerState", () => { diff --git a/website/utils/url_utils.ts b/website/utils/url_utils.ts index 95babd1b..1806963c 100644 --- a/website/utils/url_utils.ts +++ b/website/utils/url_utils.ts @@ -815,8 +815,6 @@ export async function parseViewerUrlParams(urlSearchParams: URLSearchParams): Pr args = { ...args, ...datasetArgs }; } - console.log("args", args); - console.log("viewer settings", viewerSettings); return { args: removeUndefinedProperties(args), viewerSettings: removeUndefinedProperties(viewerSettings) }; }