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;
}