Skip to content

Commit

Permalink
Merge pull request #175 from allen-cell-animated/feature/camera-mode-…
Browse files Browse the repository at this point in the history
…in-url-params

Camera mode in url params
  • Loading branch information
toloudis authored Nov 28, 2023
2 parents 31cd0be + e298c88 commit fb08725
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 65 deletions.
15 changes: 14 additions & 1 deletion public/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const VIEWER_3D_SETTINGS: ViewerChannelSettings = {
maskChannelName: "SEG_Memb",
};

type ParamKeys = "mask" | "ch" | "luts" | "colors" | "image" | "url" | "file" | "dataset" | "id";
type ParamKeys = "mask" | "ch" | "luts" | "colors" | "image" | "url" | "file" | "dataset" | "id" | "view";
type Params = { [_ in ParamKeys]?: string };

function parseQueryString(): Params {
Expand Down Expand Up @@ -101,6 +101,19 @@ if (params) {
if (params.mask) {
viewerSettings.maskAlpha = parseInt(params.mask, 10);
}
if (params.view) {
const mapping = {
"3D": ViewMode.threeD,
Z: ViewMode.xy,
Y: ViewMode.xz,
X: ViewMode.yz,
}
const allowedViews = Object.keys(mapping);
if (!allowedViews.includes(params.view)) {
params.view = "3D";
}
viewerSettings.viewMode = mapping[params.view];
}
if (params.ch) {
// ?ch=1,2
// ?luts=0,255,0,255
Expand Down
96 changes: 42 additions & 54 deletions src/aics-image-viewer/components/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const defaultViewerSettings: GlobalViewerSettings = {
levels: LEVELS_SLIDER_DEFAULT,
interpolationEnabled: INTERPOLATION_ENABLED_DEFAULT,
region: { x: [0, 1], y: [0, 1], z: [0, 1] },
slice: { x: 0.5, y: 0.5, z: 0.5 },
time: 0,
};

Expand Down Expand Up @@ -181,8 +182,6 @@ const App: React.FC<AppProps> = (props) => {
const [sendingQueryRequest, setSendingQueryRequest] = useState(false);
// `true` when all channels of the current image are loaded
const [imageLoaded, setImageLoaded] = useState(false);
// `true` when the image being loaded is related to the previous one, so some settings should be preserved
const [switchingFov, setSwitchingFov] = useState(false);
// tracks which channels have been loaded
const [channelVersions, setChannelVersions, getChannelVersions] = useStateWithGetter<number[]>([]);

Expand All @@ -205,47 +204,22 @@ const App: React.FC<AppProps> = (props) => {
// Some viewer settings require custom change behaviors to change related settings simultaneously or guard against
// entering an illegal state (e.g. autorotate must not be on in pathtrace mode). Those behaviors are defined here.
const viewerSettingsChangeHandlers: ViewerSettingChangeHandlers = {
// View mode: if we're switching to 2d, switch to volumetric rendering
viewMode: (prevSettings, viewMode) => {
if (viewMode === prevSettings.viewMode) {
return prevSettings;
}
const newSettings: GlobalViewerSettings = {
const switchToVolumetric = viewMode !== ViewMode.threeD && prevSettings.renderMode === RenderMode.pathTrace;
return {
...prevSettings,
viewMode,
region: { x: [0, 1], y: [0, 1], z: [0, 1] },
renderMode: switchToVolumetric ? RenderMode.volumetric : prevSettings.renderMode,
};
const activeAxis = activeAxisMap[viewMode];

// TODO the following behavior/logic is very specific to a particular application's needs
// and is not necessarily appropriate for a general viewer.
// Why should the alpha setting matter whether we are viewing the primary image
// or its parent?

// If switching between 2D and 3D reset alpha mask to default (off in in 2D, 50% in 3D)
// If full field, dont mask

if (activeAxis) {
// switching to 2d
const slices = Math.max(1, getNumberOfSlices()[activeAxis]);
const middleSlice = Math.floor(slices / 2);
newSettings.region[activeAxis] = [middleSlice / slices, (middleSlice + 1) / slices];
if (prevSettings.viewMode === ViewMode.threeD && newSettings.renderMode === RenderMode.pathTrace) {
// Switching from 3D to 2D
// if path trace was enabled in 3D turn it off when switching to 2D.
newSettings.renderMode = RenderMode.volumetric;
}
}
return newSettings;
},
imageType: (prevSettings, imageType) => {
setSwitchingFov(true);
return { ...prevSettings, imageType };
},
// Render mode: if we're switching to pathtrace, turn off autorotate
renderMode: (prevSettings, renderMode) => ({
...prevSettings,
renderMode,
autorotate: renderMode === RenderMode.pathTrace ? false : prevSettings.autorotate,
}),
// Autorotate: do not enable autorotate while in pathtrace mode
autorotate: (prevSettings, autorotate) => ({
...prevSettings,
// The button should theoretically be unclickable while in pathtrace mode, but this provides extra security
Expand Down Expand Up @@ -341,7 +315,6 @@ const App: React.FC<AppProps> = (props) => {
if (aimg.isLoaded()) {
view3d.updateActiveChannels(aimg);
setImageLoaded(true);
setSwitchingFov(false);
}
};

Expand Down Expand Up @@ -471,11 +444,6 @@ const App: React.FC<AppProps> = (props) => {

setAllChannelsUnloaded(channelNames.length);

// if this image is completely unrelated to the previous image, switch view mode
if (!switchingFov && !samePath) {
changeViewerSetting("viewMode", ViewMode.threeD);
}

imageUrlRef.current = fullUrl;
placeImageInViewer(aimg, newChannelSettings);
};
Expand Down Expand Up @@ -576,6 +544,11 @@ const App: React.FC<AppProps> = (props) => {
return () => window.removeEventListener("resize", onResizeDebounced);
}, []);

// one-time init after view3d exists and before we start loading images
useEffect(() => {
view3d.setCameraMode(viewerSettings.viewMode);
}, []);

// Hook to trigger image load: on mount, when image source props/state change (`cellId`, `imageType`, `time`)
useEffect(() => {
if (props.rawDims && props.rawData) {
Expand Down Expand Up @@ -711,26 +684,40 @@ const App: React.FC<AppProps> = (props) => {
[props.transform?.rotation]
);

const usePerAxisClippingUpdater = (axis: AxisName, [minval, maxval]: [number, number]): void => {
const usePerAxisClippingUpdater = (axis: AxisName, [minval, maxval]: [number, number], slice: number): void => {
useImageEffect(
// Logic to determine axis clipping range, for each of x,y,z,3d slider:
// if slider was same as active axis view mode: [viewerSettings.slice[axis], viewerSettings.slice[axis] + 1.0/volumeSize[axis]]
// if in 3d mode: viewerSettings.region[axis]
// else: [0,1]
(currentImage) => {
const isOrthoAxis = activeAxisMap[viewerSettings.viewMode] === axis;
view3d.setAxisClip(currentImage, axis, minval - 0.5, maxval - 0.5, isOrthoAxis);
let isOrthoAxis = false;
let axismin = 0.0;
let axismax = 1.0;
if (viewerSettings.viewMode === ViewMode.threeD) {
axismin = minval;
axismax = maxval;
isOrthoAxis = false;
} else {
isOrthoAxis = activeAxisMap[viewerSettings.viewMode] === axis;
const oneSlice = 1 / currentImage.imageInfo.volumeSize[axis];
axismin = isOrthoAxis ? slice : 0.0;
axismax = isOrthoAxis ? slice + oneSlice : 1.0;
if (axis === "z" && viewerSettings.viewMode === ViewMode.xy) {
view3d.setZSlice(currentImage, Math.floor(slice * currentImage.imageInfo.volumeSize.z));
}
}
// view3d wants the coordinates in the -0.5 to 0.5 range
view3d.setAxisClip(currentImage, axis, axismin - 0.5, axismax - 0.5, isOrthoAxis);
view3d.setCameraMode(viewerSettings.viewMode);
},
[minval, maxval]
[minval, maxval, slice, viewerSettings.viewMode]
);
};
usePerAxisClippingUpdater("x", viewerSettings.region.x);
usePerAxisClippingUpdater("y", viewerSettings.region.y);
usePerAxisClippingUpdater("z", viewerSettings.region.z);
// Z slice is a separate property that also must be updated
useImageEffect(
(currentImage) => {
const slice = Math.floor(viewerSettings.region.z[0] * currentImage.imageInfo.volumeSize.z);
view3d.setZSlice(currentImage, slice);
},
[viewerSettings.region.z[0]]
);

usePerAxisClippingUpdater("x", viewerSettings.region.x, viewerSettings.slice.x);
usePerAxisClippingUpdater("y", viewerSettings.region.y, viewerSettings.slice.y);
usePerAxisClippingUpdater("z", viewerSettings.region.z, viewerSettings.slice.z);

// Rendering ////////////////////////////////////////////////////////////////

Expand Down Expand Up @@ -815,6 +802,7 @@ const App: React.FC<AppProps> = (props) => {
numSlices={getNumberOfSlices()}
numTimesteps={numberOfTimesteps}
region={viewerSettings.region}
slices={viewerSettings.slice}
time={viewerSettings.time}
appHeight={props.appHeight}
showControls={showControls}
Expand Down
6 changes: 5 additions & 1 deletion src/aics-image-viewer/components/App/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,12 @@ export interface GlobalViewerSettings {
levels: [number, number, number];
interpolationEnabled: boolean;
// `region` values are in the range [0, 1]. We derive from this the format that the sliders expect
// (integers between 0 and num_slices - 1) and the format that view3d expects (in [-0.5, 0.5])
// (integers between 0 and num_slices - 1) and the format that view3d expects (in [-0.5, 0.5]).
// This state is only active in 3d mode.
region: PerAxis<[number, number]>;
// Store the relative position of the slice in the range [0, 1] for each of 3 axes.
// This state is active in x,y,z single slice modes.
slice: PerAxis<number>;
time: number;
}

Expand Down
28 changes: 20 additions & 8 deletions src/aics-image-viewer/components/AxisClipSliders/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ interface AxisClipSlidersProps {
changeViewerSetting: ViewerSettingUpdater;
numSlices: PerAxis<number>;
region: PerAxis<[number, number]>;
slices: PerAxis<number>;
numTimesteps: number;
time: number;
}
Expand Down Expand Up @@ -134,9 +135,9 @@ export default class AxisClipSliders extends React.Component<AxisClipSlidersProp
}
const delta = backward ? -1 : 1;
const max = this.props.numSlices[activeAxis];
const currentLeftSliderValue = Math.round(this.props.region[activeAxis][0] * max);
const currentLeftSliderValue = Math.round(this.props.slices[activeAxis] * max);
const leftValue = (currentLeftSliderValue + delta + max) % max;
this.updateClipping(activeAxis, leftValue, leftValue + 1);
this.updateSlice(activeAxis, leftValue);
}

play(): void {
Expand All @@ -153,35 +154,46 @@ export default class AxisClipSliders extends React.Component<AxisClipSlidersProp
}
}

updateClipping(axis: AxisName, minval: number, maxval: number): void {
updateRegion(axis: AxisName, minval: number, maxval: number): void {
const { changeViewerSetting, numSlices, region } = this.props;
// get a value from -0.5..0.5
// get a value from 0-1
const max = numSlices[axis];
const start = minval / max;
const end = maxval / max;
changeViewerSetting("region", { ...region, [axis]: [start, end] });
}

updateSlice(axis: AxisName, val: number): void {
const { changeViewerSetting, numSlices, slices } = this.props;
changeViewerSetting("slice", { ...slices, [axis]: val / numSlices[axis] });
}

makeSliderCallback(axis: AxisName): (values: number[]) => void {
return (values: number[]) => {
const max = values.length < 2 ? values[0] + 1 : values[1];
this.updateClipping(axis, values[0], max);
if (values.length < 2) {
this.updateSlice(axis, values[0]);
} else {
this.updateRegion(axis, values[0], values[1]);
}
};
}

createAxisSlider(axis: AxisName, twoD: boolean): React.ReactNode {
const { playing } = this.state;
const numSlices = this.props.numSlices[axis];
const clipVals = this.props.region[axis];
const sliderVals = [Math.round(clipVals[0] * numSlices), Math.round(clipVals[1] * numSlices)];
const slice = this.props.slices[axis];
const sliderVals = twoD
? [Math.round(slice * numSlices)]
: [Math.round(clipVals[0] * numSlices), Math.round(clipVals[1] * numSlices)];

return (
<div key={axis + numSlices} className={`slider-row slider-${axis}`}>
<SliderRow
// prevents slider from potentially not updating number of handles
key={`${twoD}`}
label={axis.toUpperCase()}
vals={twoD ? [sliderVals[0]] : sliderVals}
vals={sliderVals}
max={numSlices - (twoD && numSlices > 1 ? 1 : 0)}
onSlide={this.makeSliderCallback(axis)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface ViewerWrapperProps {
hasImage: boolean;
numSlices: PerAxis<number>;
region: PerAxis<[number, number]>;
slices: PerAxis<number>;
numTimesteps: number;
time: number;
showControls: {
Expand Down Expand Up @@ -65,7 +66,7 @@ export default class ViewerWrapper extends React.Component<ViewerWrapperProps, V
}

render(): React.ReactNode {
const { appHeight, changeViewerSetting, showControls, numSlices, numTimesteps, viewMode, region, time } =
const { appHeight, changeViewerSetting, showControls, numSlices, numTimesteps, viewMode, region, slices, time } =
this.props;

return (
Expand All @@ -82,6 +83,7 @@ export default class ViewerWrapper extends React.Component<ViewerWrapperProps, V
changeViewerSetting={changeViewerSetting}
numSlices={numSlices}
region={region}
slices={slices}
numTimesteps={numTimesteps}
time={time}
/>
Expand Down

0 comments on commit fb08725

Please sign in to comment.