diff --git a/public/index.html b/public/index.html index cba8fd86..816fba75 100644 --- a/public/index.html +++ b/public/index.html @@ -1,29 +1,62 @@ - - - + + + + + + + AICS 3D Volume Viewer - - - - - - + + + + + + + + +
- + diff --git a/src/aics-image-viewer/shared/constants.ts b/src/aics-image-viewer/shared/constants.ts index f9765090..1a5ff7cb 100644 --- a/src/aics-image-viewer/shared/constants.ts +++ b/src/aics-image-viewer/shared/constants.ts @@ -1,3 +1,5 @@ +import { CameraState } from "@aics/volume-viewer"; + import { ChannelState, ViewerState } from "../components/ViewerStateProvider/types"; import { ViewMode, RenderMode, ImageType } from "./enums"; import { ColorArray } from "./utils/colorRepresentations"; @@ -93,6 +95,22 @@ export const PRESET_COLOR_MAP = Object.freeze([ }, ]); +/** Allows the 3D viewer to apply the default camera settings for the view mode. */ +const USE_VIEW_MODE_DEFAULT_CAMERA = undefined; + +/** + * Reflects the default camera settings the 3D viewer uses on volume load. + * These SHOULD NOT be changed; otherwise, existing shared links that don't specify the + * camera settings will use the new defaults and may be in unexpected orientations or positions. + */ +export const getDefaultCameraState = (): CameraState => ({ + position: [0, 0, 5], + target: [0, 0, 0], + up: [0, 1, 0], + fov: 20, + orthoScale: 0.5, +}); + export const getDefaultViewerState = (): ViewerState => ({ viewMode: ViewMode.threeD, // "XY", "XZ", "YZ" renderMode: RenderMode.volumetric, // "pathtrace", "maxproject" @@ -110,13 +128,11 @@ export const getDefaultViewerState = (): ViewerState => ({ region: { x: [0, 1], y: [0, 1], z: [0, 1] }, slice: { x: 0.5, y: 0.5, z: 0.5 }, time: 0, - cameraState: { - position: [0, 0, 5], - target: [0, 0, 0], - up: [0, 1, 0], - fov: 20, - orthoScale: 0.5, - }, + // Do not override camera position, target, etc. by default; + // instead, let the viewer apply default camera settings based on the view mode. + // This prevents a bug where the camera's position and view mode are set to + // incompatible states and the viewport becomes blank. + cameraState: USE_VIEW_MODE_DEFAULT_CAMERA, }); export const getDefaultChannelState = (): ChannelState => ({ diff --git a/website/utils/test/url_utils.test.ts b/website/utils/test/url_utils.test.ts index 2498ca41..2b588000 100644 --- a/website/utils/test/url_utils.test.ts +++ b/website/utils/test/url_utils.test.ts @@ -1,3 +1,4 @@ +import { CameraState } from "@aics/volume-viewer"; import { describe, expect, it } from "@jest/globals"; import { @@ -16,11 +17,16 @@ import { serializeViewerUrlParams, CONTROL_POINTS_REGEX, LEGACY_CONTROL_POINTS_REGEX, + serializeCameraState, } from "../url_utils"; import { ChannelState, ViewerState } from "../../../src/aics-image-viewer/components/ViewerStateProvider/types"; import { ImageType, RenderMode, ViewMode } from "../../../src/aics-image-viewer/shared/enums"; import { ViewerChannelSetting } from "../../../src/aics-image-viewer/shared/utils/viewerChannelSettings"; -import { getDefaultChannelState, getDefaultViewerState } from "../../../src/aics-image-viewer/shared/constants"; +import { + getDefaultCameraState, + getDefaultChannelState, + getDefaultViewerState, +} from "../../../src/aics-image-viewer/shared/constants"; const defaultSettings: ViewerChannelSetting = { match: 0, @@ -390,7 +396,7 @@ describe("Channel state serialization", () => { }); }); -describe("Viewer state serialization", () => { +describe("Viewer state", () => { const DEFAULT_VIEWER_STATE: ViewerState = { viewMode: ViewMode.threeD, // "XY", "XZ", "YZ" renderMode: RenderMode.volumetric, // "pathtrace", "maxproject" @@ -540,6 +546,32 @@ describe("Viewer state serialization", () => { }); }); +describe("Camera state", () => { + it("uses default camera state when choosing elements to exclude/ignore", () => { + let cameraState: CameraState = { + ...getDefaultCameraState(), + }; + // No changes from default + expect(serializeCameraState(cameraState, true)).toEqual(undefined); + + cameraState = { ...cameraState, position: [1, 2, 3] }; + expect(serializeCameraState(cameraState, true)).toEqual("pos:1:2:3"); + }); + + it("default camera state has not been changed", () => { + // The default camera state should NOT change unless backwards compatibility + // is added to ensure old links still maintain the same camera orientation; + // otherwise, cameras will appear in the new default orientation unexpectedly. + expect(getDefaultCameraState()).toEqual({ + position: [0, 0, 5], + target: [0, 0, 0], + up: [0, 1, 0], + fov: 20, + orthoScale: 0.5, + }); + }); +}); + //// DESERIALIZE STATES /////////////////////// describe("Channel state deserialization", () => { diff --git a/website/utils/url_utils.ts b/website/utils/url_utils.ts index 5a9b3fee..c2d55226 100644 --- a/website/utils/url_utils.ts +++ b/website/utils/url_utils.ts @@ -17,7 +17,11 @@ import { PerAxis } from "../../src/aics-image-viewer/shared/types"; import { clamp } from "./math_utils"; import { removeMatchingProperties, removeUndefinedProperties } from "./datatype_utils"; import { isEqual } from "lodash"; -import { getDefaultChannelState, getDefaultViewerState } from "../../src/aics-image-viewer/shared/constants"; +import { + getDefaultCameraState, + getDefaultChannelState, + getDefaultViewerState, +} from "../../src/aics-image-viewer/shared/constants"; export const ENCODED_COMMA_REGEX = /%2C/g; export const ENCODED_COLON_REGEX = /%3A/g; @@ -591,9 +595,12 @@ function parseCameraState(cameraSettings: string | undefined): Partial, removeDefaults: boolean): string | undefined { +export function serializeCameraState(cameraState: Partial, removeDefaults: boolean): string | undefined { if (removeDefaults) { - cameraState = removeMatchingProperties(cameraState, getDefaultViewerState().cameraState ?? {}); + // Note that we use the `getDefaultCameraState()` to get the defaults here, + // instead of `getDefaultViewerState().cameraState`. The latter is undefined, which signals + // that the camera should not be modified for URLs that don't specify it. + cameraState = removeMatchingProperties(cameraState, getDefaultCameraState()); if (Object.keys(cameraState).length === 0) { return undefined; }