From 8fca7c55eb8a254974669db1321baf2b80611cac Mon Sep 17 00:00:00 2001 From: Andy Sweet Date: Thu, 6 Jun 2024 09:21:11 -0700 Subject: [PATCH] Store selection state in URL hash (#95) Closes #49 Closes #62 (by making it no longer relevant) This stores selection state in the URL hash. In order to make this work, we need some state that reflects the selected points/cells/tracks independent of `PointCanvas.curTime` because the ordering of the React effects when re-hydrating from the entire state can be different compared to when the user is interacting with the application. Previously, the canvas stored the last selected point indices, which is transient state that depends on `PointCanvas.curTime`. Here we stored the all the point IDs that were selected by the user (inspired from #93). Alternatively, we considered storing the track IDs that were selected in #91 , but that doesn't work as well with the rest of the application logic. This PR also updates the URL hash to store some simpler state that not been added like point brightness. It also refactors that code a little to make it easier to keep new state in sync. Though there is still a lot of duplication that could be improved. It does not fix #30 , which is related, but can be solved independently. --- src/components/App.tsx | 88 ++++++++++++++-------------------- src/components/Scene.tsx | 2 - src/hooks/usePointCanvas.ts | 81 ++++++++++++++++++------------- src/lib/BoxPointSelector.ts | 15 +++--- src/lib/PointCanvas.ts | 58 +++++++++++++++++----- src/lib/PointSelector.ts | 20 ++++---- src/lib/SpherePointSelector.ts | 6 +-- src/lib/ViewerState.ts | 44 ++++++++--------- 8 files changed, 168 insertions(+), 146 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index d58fc9d4..3bfaf99b 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -19,7 +19,7 @@ import { ColorMap } from "./overlays/ColorMap"; // Ideally we do this here so that we can use initial values as default values for React state. const initialViewerState = ViewerState.fromUrlHash(window.location.hash); -console.log("initial viewer state: %s", JSON.stringify(initialViewerState)); +console.log("initial viewer state: ", initialViewerState); clearUrlHash(); const drawerWidth = 256; @@ -32,7 +32,6 @@ export default function App() { const numTimes = trackManager?.numTimes ?? 0; // TODO: dataUrl can be stored in the TrackManager only const [dataUrl, setDataUrl] = useState(initialViewerState.dataUrl); - const [isLoadingTracks, setIsLoadingTracks] = useState(false); // PointCanvas is a Three.js canvas, updated via reducer const [canvas, dispatchCanvas, sceneDivRef] = usePointCanvas(initialViewerState); @@ -42,25 +41,23 @@ export default function App() { // this state is pure React const [playing, setPlaying] = useState(false); const [isLoadingPoints, setIsLoadingPoints] = useState(false); + const [numLoadingTracks, setNumLoadingTracks] = useState(0); // Manage shareable state that can persist across sessions. const copyShareableUrlToClipboard = () => { console.log("copy shareable URL to clipboard"); - const state = new ViewerState(dataUrl, canvas.curTime, canvas.camera.position, canvas.controls.target); - const url = window.location.toString() + "#" + state.toUrlHash(); + const state = canvas.toState(); + if (trackManager) { + state.dataUrl = trackManager.store; + } + const url = window.location.toString() + state.toUrlHash(); navigator.clipboard.writeText(url); }; - const setStateFromHash = useCallback(() => { const state = ViewerState.fromUrlHash(window.location.hash); clearUrlHash(); setDataUrl(state.dataUrl); - dispatchCanvas({ type: ActionType.CUR_TIME, curTime: state.curTime }); - dispatchCanvas({ - type: ActionType.CAMERA_PROPERTIES, - cameraPosition: state.cameraPosition, - cameraTarget: state.cameraTarget, - }); + dispatchCanvas({ type: ActionType.UPDATE_WITH_STATE, state: state }); }, [dispatchCanvas]); // update the state when the hash changes, but only register the listener once @@ -143,48 +140,40 @@ export default function App() { }; }, [canvas.curTime, dispatchCanvas, trackManager]); + // This fetches track IDs based on the selected point IDs. useEffect(() => { - console.debug("effect-selection"); - const pointsID = canvas.points.id; - const selectedPoints = canvas.selectedPoints; - if (!selectedPoints || !selectedPoints.has(pointsID)) return; - // keep track of which tracks we are adding to avoid duplicate fetching - const adding = new Set(); + console.debug("effect-selectedPointIds: ", trackManager, canvas.selectedPointIds); + if (!trackManager) return; + if (canvas.selectedPointIds.size == 0) return; // this fetches the entire lineage for each track - const fetchAndAddTrack = async (pointID: number) => { - if (!trackManager) return; - const tracks = await trackManager.fetchTrackIDsForPoint(pointID); - // TODO: points actually only belong to one track, so can get rid of the outer loop - for (const t of tracks) { - const lineage = await trackManager.fetchLineageForTrack(t); - for (const l of lineage) { - if (adding.has(l) || canvas.tracks.has(l)) continue; - adding.add(l); - const [pos, ids] = await trackManager.fetchPointsForTrack(l); - // adding the track *in* the dispatcher creates issues with duplicate fetching - // but we refresh so the selected/loaded count is updated - canvas.addTrack(l, pos, ids); - dispatchCanvas({ type: ActionType.REFRESH }); + const updateTracks = async () => { + console.debug("updateTracks: ", canvas.selectedPointIds); + for (const pointId of canvas.selectedPointIds) { + if (canvas.fetchedPointIds.has(pointId)) continue; + setNumLoadingTracks((n) => n + 1); + canvas.fetchedPointIds.add(pointId); + const trackIds = await trackManager.fetchTrackIDsForPoint(pointId); + // TODO: points actually only belong to one track, so can get rid of the outer loop + for (const trackId of trackIds) { + if (canvas.fetchedRootTrackIds.has(trackId)) continue; + canvas.fetchedRootTrackIds.add(trackId); + const lineage = await trackManager.fetchLineageForTrack(trackId); + for (const relatedTrackId of lineage) { + if (canvas.tracks.has(relatedTrackId)) continue; + const [pos, ids] = await trackManager.fetchPointsForTrack(relatedTrackId); + // adding the track *in* the dispatcher creates issues with duplicate fetching + // but we refresh so the selected/loaded count is updated + canvas.addTrack(relatedTrackId, pos, ids); + dispatchCanvas({ type: ActionType.REFRESH }); + } } + setNumLoadingTracks((n) => n - 1); } }; - - dispatchCanvas({ type: ActionType.POINT_BRIGHTNESS, brightness: 0.8 }); - - const selected = selectedPoints.get(pointsID) || []; - dispatchCanvas({ type: ActionType.HIGHLIGHT_POINTS, points: selected }); - - const maxPointsPerTimepoint = trackManager?.maxPointsPerTimepoint ?? 0; - - setIsLoadingTracks(true); - Promise.all(selected.map((p: number) => canvas.curTime * maxPointsPerTimepoint + p).map(fetchAndAddTrack)).then( - () => { - setIsLoadingTracks(false); - }, - ); + updateTracks(); // TODO: add missing dependencies - }, [canvas.selectedPoints]); + }, [trackManager, dispatchCanvas, canvas.selectedPointIds]); // playback time points // TODO: this is basic and may drop frames @@ -302,12 +291,7 @@ export default function App() { overflow: "hidden", }} > - + 0} /> diff --git a/src/components/Scene.tsx b/src/components/Scene.tsx index c9956813..29e80709 100644 --- a/src/components/Scene.tsx +++ b/src/components/Scene.tsx @@ -5,8 +5,6 @@ import { Box } from "@mui/material"; interface SceneProps { isLoading: boolean; - initialCameraPosition?: THREE.Vector3; - initialCameraTarget?: THREE.Vector3; } const Scene = forwardRef(function SceneRender(props: SceneProps, ref: React.Ref) { diff --git a/src/hooks/usePointCanvas.ts b/src/hooks/usePointCanvas.ts index 093d02a9..d69a64f4 100644 --- a/src/hooks/usePointCanvas.ts +++ b/src/hooks/usePointCanvas.ts @@ -1,17 +1,12 @@ import { useCallback, useEffect, useReducer, useRef, Dispatch, RefObject } from "react"; -import { Vector3 } from "three"; - import { PointCanvas } from "@/lib/PointCanvas"; -import { PointsCollection } from "@/lib/PointSelectionBox"; import { PointSelectionMode } from "@/lib/PointSelector"; import { ViewerState } from "@/lib/ViewerState"; enum ActionType { AUTO_ROTATE = "AUTO_ROTATE", - CAMERA_PROPERTIES = "CAMERA_PROPERTIES", CUR_TIME = "CUR_TIME", - HIGHLIGHT_POINTS = "HIGHLIGHT_POINTS", INIT_POINTS_GEOMETRY = "INIT_POINTS_GEOMETRY", POINT_BRIGHTNESS = "POINT_BRIGHTNESS", POINTS_POSITIONS = "POINTS_POSITIONS", @@ -22,6 +17,8 @@ enum ActionType { SHOW_TRACK_HIGHLIGHTS = "SHOW_TRACK_HIGHLIGHTS", SIZE = "SIZE", MIN_MAX_TIME = "MIN_MAX_TIME", + ADD_SELECTED_POINT_IDS = "ADD_SELECTED_POINT_IDS", + UPDATE_WITH_STATE = "UPDATE_WITH_STATE", } interface AutoRotate { @@ -29,22 +26,11 @@ interface AutoRotate { autoRotate: boolean; } -interface CameraProperties { - type: ActionType.CAMERA_PROPERTIES; - cameraPosition: Vector3; - cameraTarget: Vector3; -} - interface CurTime { type: ActionType.CUR_TIME; curTime: number | ((curTime: number) => number); } -interface HighlightPoints { - type: ActionType.HIGHLIGHT_POINTS; - points: number[]; -} - interface InitPointsGeometry { type: ActionType.INIT_POINTS_GEOMETRY; maxPointsPerTimepoint: number; @@ -95,12 +81,21 @@ interface MinMaxTime { maxTime: number; } +interface AddSelectedPointIds { + type: ActionType.ADD_SELECTED_POINT_IDS; + selectedPointIndices: number[]; + selectedPointIds: Set; +} + +interface UpdateWithState { + type: ActionType.UPDATE_WITH_STATE; + state: ViewerState; +} + // setting up a tagged union for the actions type PointCanvasAction = | AutoRotate - | CameraProperties | CurTime - | HighlightPoints | InitPointsGeometry | PointBrightness | PointsPositions @@ -110,7 +105,9 @@ type PointCanvasAction = | ShowTracks | ShowTrackHighlights | Size - | MinMaxTime; + | MinMaxTime + | AddSelectedPointIds + | UpdateWithState; function reducer(canvas: PointCanvas, action: PointCanvasAction): PointCanvas { console.debug("usePointCanvas.reducer: ", action); @@ -118,9 +115,6 @@ function reducer(canvas: PointCanvas, action: PointCanvasAction): PointCanvas { switch (action.type) { case ActionType.REFRESH: break; - case ActionType.CAMERA_PROPERTIES: - newCanvas.setCameraProperties(action.cameraPosition, action.cameraTarget); - break; case ActionType.CUR_TIME: { // if curTime is a function, call it with the current time if (typeof action.curTime === "function") { @@ -135,9 +129,6 @@ function reducer(canvas: PointCanvas, action: PointCanvasAction): PointCanvas { case ActionType.AUTO_ROTATE: newCanvas.controls.autoRotate = action.autoRotate; break; - case ActionType.HIGHLIGHT_POINTS: - newCanvas.highlightPoints(action.points); - break; case ActionType.INIT_POINTS_GEOMETRY: newCanvas.initPointsGeometry(action.maxPointsPerTimepoint); break; @@ -173,6 +164,22 @@ function reducer(canvas: PointCanvas, action: PointCanvasAction): PointCanvas { newCanvas.maxTime = action.maxTime; newCanvas.updateAllTrackHighlights(); break; + case ActionType.ADD_SELECTED_POINT_IDS: { + newCanvas.pointBrightness = 0.8; + newCanvas.resetPointColors(); + // TODO: only highlight the indices if the canvas is at the same time + // point as when it was selected. + newCanvas.highlightPoints(action.selectedPointIndices); + const newSelectedPointIds = new Set(canvas.selectedPointIds); + for (const trackId of action.selectedPointIds) { + newSelectedPointIds.add(trackId); + } + newCanvas.selectedPointIds = newSelectedPointIds; + break; + } + case ActionType.UPDATE_WITH_STATE: + newCanvas.updateWithState(action.state); + break; default: console.warn("usePointCanvas reducer - unknown action type: %s", action); return canvas; @@ -181,12 +188,13 @@ function reducer(canvas: PointCanvas, action: PointCanvasAction): PointCanvas { } function createPointCanvas(initialViewerState: ViewerState): PointCanvas { + console.debug("createPointCanvas: ", initialViewerState); // create the canvas with some default dimensions // these will be overridden when the canvas is inserted into a div const canvas = new PointCanvas(800, 600); - // restore canvas from initial viewer state - canvas.setCameraProperties(initialViewerState.cameraPosition, initialViewerState.cameraTarget); + // Update the state from any initial values. + canvas.updateWithState(initialViewerState); // start animating - this keeps the scene rendering when controls change, etc. canvas.animate(); @@ -197,16 +205,23 @@ function createPointCanvas(initialViewerState: ViewerState): PointCanvas { function usePointCanvas( initialViewerState: ViewerState, ): [PointCanvas, Dispatch, RefObject] { - console.debug("usePointCanvas: ", initialViewerState); const divRef = useRef(null); const [canvas, dispatchCanvas] = useReducer(reducer, initialViewerState, createPointCanvas); // When the selection changes internally due to the user interacting with the canvas, - // we need to trigger a react re-render. - canvas.selector.selectionChanged = useCallback((_selection: PointsCollection) => { - console.debug("selectionChanged: refresh"); - dispatchCanvas({ type: ActionType.REFRESH }); - }, []); + // we need to dispatch an addition to the canvas' state. + canvas.selector.selectionChanged = useCallback( + (pointIndices: number[]) => { + console.debug("selectionChanged:", pointIndices); + const pointIds = new Set(pointIndices.map((p) => canvas.curTime * canvas.maxPointsPerTimepoint + p)); + dispatchCanvas({ + type: ActionType.ADD_SELECTED_POINT_IDS, + selectedPointIndices: pointIndices, + selectedPointIds: pointIds, + }); + }, + [canvas.curTime, canvas.maxPointsPerTimepoint], + ); // set up the canvas when the div is available // this is an effect because: diff --git a/src/lib/BoxPointSelector.ts b/src/lib/BoxPointSelector.ts index 26cb6192..27d26d1e 100644 --- a/src/lib/BoxPointSelector.ts +++ b/src/lib/BoxPointSelector.ts @@ -1,8 +1,8 @@ -import { PerspectiveCamera, Scene, WebGLRenderer } from "three"; +import { PerspectiveCamera, Points, Scene, WebGLRenderer } from "three"; import { OrbitControls } from "three/addons/controls/OrbitControls.js"; import { SelectionHelper } from "three/addons/interactive/SelectionHelper.js"; -import { PointSelectionBox, PointsCollection } from "@/lib/PointSelectionBox"; +import { PointSelectionBox } from "@/lib/PointSelectionBox"; import { SelectionChanged } from "@/lib/PointSelector"; // Selection with a 2D rectangle to make a 3D frustum. @@ -11,6 +11,7 @@ export class BoxPointSelector { readonly controls: OrbitControls; readonly box: PointSelectionBox; readonly helper: SelectionHelper; + readonly points: Points; readonly selectionChanged: SelectionChanged; // True if this should not perform selection, false otherwise. @@ -23,10 +24,12 @@ export class BoxPointSelector { renderer: WebGLRenderer, camera: PerspectiveCamera, controls: OrbitControls, + points: Points, selectionChanged: SelectionChanged, ) { this.renderer = renderer; this.controls = controls; + this.points = points; this.helper = new SelectionHelper(renderer, "selectBox"); this.helper.enabled = false; this.box = new PointSelectionBox(camera, scene); @@ -49,12 +52,6 @@ export class BoxPointSelector { } } - setSelectedPoints(selectedPoints: PointsCollection) { - console.debug("BoxPointSelector.setSelectedPoints: ", selectedPoints); - this.box.collection = selectedPoints; - this.selectionChanged(selectedPoints); - } - pointerUp(_event: MouseEvent) { console.debug("BoxPointSelector.pointerUp"); this.blocked = false; @@ -78,7 +75,7 @@ export class BoxPointSelector { // TODO: consider restricting selection to a specific object this.box.select(); - this.setSelectedPoints(this.box.collection); + this.selectionChanged(this.box.collection.get(this.points.id) ?? []); } pointerCancel(_event: MouseEvent) { diff --git a/src/lib/PointCanvas.ts b/src/lib/PointCanvas.ts index 6633eb67..11516acd 100644 --- a/src/lib/PointCanvas.ts +++ b/src/lib/PointCanvas.ts @@ -12,7 +12,6 @@ import { SRGBColorSpace, TextureLoader, Vector2, - Vector3, WebGLRenderer, } from "three"; import { OrbitControls } from "three/addons/controls/OrbitControls.js"; @@ -23,7 +22,7 @@ import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js" import { Track } from "@/lib/three/Track"; import { PointSelector, PointSelectionMode } from "@/lib/PointSelector"; -import { PointsCollection } from "@/lib/PointSelectionBox"; +import { ViewerState } from "./ViewerState"; type Tracks = Map; @@ -37,19 +36,32 @@ export class PointCanvas { readonly bloomPass: UnrealBloomPass; readonly selector: PointSelector; + // Maps from track ID to three.js Track objects. + // This contains all tracks or tracklets across the lineages of all + // selected cells. readonly tracks: Tracks = new Map(); + // Needed to skip fetches for lineages that have already been fetched. + // TODO: storing the fetched track and point IDs here works for now, + // but is likely a good candidate for a refactor. + readonly fetchedRootTrackIds = new Set(); + // Needed to skip fetches for point IDs that been selected. + readonly fetchedPointIds = new Set(); + // All the point IDs that have been selected. + // PointCanvas.selector.selection is the transient array of selected + // point indices associated with a specific time point and selection action, + // whereas these are a union of all those selection actions, are unique + // across the whole dataset and can be used for persistent storage. + selectedPointIds: Set = new Set(); showTracks = true; showTrackHighlights = true; curTime: number = 0; minTime: number = -6; maxTime: number = 5; pointBrightness = 1.0; - // this is used to initialize the points geometry, and kept to initialize the // tracks but could be pulled from the points geometry when adding tracks - // private here to consolidate external access via `TrackManager` instead - private maxPointsPerTimepoint = 0; + maxPointsPerTimepoint = 0; constructor(width: number, height: number) { this.scene = new Scene(); @@ -107,8 +119,32 @@ export class PointCanvas { return newCanvas as PointCanvas; } - get selectedPoints(): PointsCollection { - return this.selector.selection; + toState(): ViewerState { + const state = new ViewerState(); + state.curTime = this.curTime; + state.minTime = this.minTime; + state.maxTime = this.maxTime; + state.maxPointsPerTimepoint = this.maxPointsPerTimepoint; + state.pointBrightness = this.pointBrightness; + state.showTracks = this.showTracks; + state.showTrackHighlights = this.showTrackHighlights; + state.selectedPointIds = new Array(...this.selectedPointIds); + state.cameraPosition = this.camera.position.toArray(); + state.cameraTarget = this.controls.target.toArray(); + return state; + } + + updateWithState(state: ViewerState) { + this.curTime = state.curTime; + this.minTime = state.minTime; + this.maxTime = state.maxTime; + this.maxPointsPerTimepoint = state.maxPointsPerTimepoint; + this.pointBrightness = state.pointBrightness; + this.showTracks = state.showTracks; + this.showTrackHighlights = state.showTrackHighlights; + this.selectedPointIds = new Set(state.selectedPointIds); + this.camera.position.fromArray(state.cameraPosition); + this.controls.target.fromArray(state.cameraTarget); } setSelectionMode(mode: PointSelectionMode) { @@ -124,11 +160,6 @@ export class PointCanvas { this.controls.update(); }; - setCameraProperties(position?: Vector3, target?: Vector3) { - position && this.camera.position.set(position.x, position.y, position.z); - target && this.controls.target.set(target.x, target.y, target.z); - } - highlightPoints(points: number[]) { const colorAttribute = this.points.geometry.getAttribute("color"); const color = new Color(); @@ -222,6 +253,9 @@ export class PointCanvas { } removeAllTracks() { + this.selectedPointIds = new Set(); + this.fetchedRootTrackIds.clear(); + this.fetchedPointIds.clear(); for (const trackID of this.tracks.keys()) { this.removeTrack(trackID); } diff --git a/src/lib/PointSelector.ts b/src/lib/PointSelector.ts index 4feb1715..f82b2704 100644 --- a/src/lib/PointSelector.ts +++ b/src/lib/PointSelector.ts @@ -1,7 +1,6 @@ import { PerspectiveCamera, Points, Scene, WebGLRenderer } from "three"; import { OrbitControls } from "three/addons/controls/OrbitControls.js"; -import { PointsCollection } from "@/lib/PointSelectionBox"; import { BoxPointSelector } from "./BoxPointSelector"; import { SpherePointSelector } from "./SpherePointSelector"; @@ -22,7 +21,7 @@ interface PointSelectorInterface { dispose(): void; } -export type SelectionChanged = (selection: PointsCollection) => void; +export type SelectionChanged = (selection: number[]) => void; // this is a separate class to keep the point selection logic separate from the rendering logic in // the PointCanvas class this fixes some issues with callbacks and event listeners binding to @@ -34,9 +33,8 @@ export class PointSelector { readonly sphereSelector: SpherePointSelector; selectionMode: PointSelectionMode = PointSelectionMode.BOX; - selection: PointsCollection = new Map(); - // To optionally notify external observers about changes to the current selection. - selectionChanged: SelectionChanged = (_selection: PointsCollection) => {}; + // To notify external observers about changes to the current selection. + selectionChanged: SelectionChanged = (_selection: number[]) => {}; constructor( scene: Scene, @@ -45,7 +43,14 @@ export class PointSelector { controls: OrbitControls, points: Points, ) { - this.boxSelector = new BoxPointSelector(scene, renderer, camera, controls, this.setSelectedPoints.bind(this)); + this.boxSelector = new BoxPointSelector( + scene, + renderer, + camera, + controls, + points, + this.setSelectedPoints.bind(this), + ); this.sphereSelector = new SpherePointSelector( scene, renderer, @@ -84,9 +89,8 @@ export class PointSelector { return this.selectionMode === PointSelectionMode.BOX ? this.boxSelector : this.sphereSelector; } - setSelectedPoints(selection: PointsCollection) { + setSelectedPoints(selection: number[]) { console.debug("PointSelector.setSelectedPoints:", selection); - this.selection = selection; this.selectionChanged(selection); } diff --git a/src/lib/SpherePointSelector.ts b/src/lib/SpherePointSelector.ts index aad6e923..cdf1225c 100644 --- a/src/lib/SpherePointSelector.ts +++ b/src/lib/SpherePointSelector.ts @@ -14,8 +14,6 @@ import { import { OrbitControls } from "three/addons/controls/OrbitControls.js"; import { TransformControls } from "three/examples/jsm/Addons.js"; -import { PointsCollection } from "@/lib/PointSelectionBox"; - import { SelectionChanged } from "@/lib/PointSelector"; // Selecting with a sphere, with optional transform controls. @@ -175,10 +173,8 @@ export class SpherePointSelector { selected.push(i); } } - const points: PointsCollection = new Map(); - points.set(this.points.id, selected); console.log("selected points:", selected); - this.selectionChanged(points); + this.selectionChanged(selected); } pointerDown(_event: MouseEvent) {} diff --git a/src/lib/ViewerState.ts b/src/lib/ViewerState.ts index b309ecf9..8fd63b40 100644 --- a/src/lib/ViewerState.ts +++ b/src/lib/ViewerState.ts @@ -1,5 +1,3 @@ -import { Vector3 } from "three"; - export const DEFAULT_ZARR_URL = "https://sci-imaging-vis-public-demo-data.s3.us-west-2.amazonaws.com" + "/points-web-viewer/sparse-zarr-v2/ZSNS001_tracks_bundle.zarr"; @@ -16,41 +14,37 @@ export function clearUrlHash() { // Encapsulates all the persistent state in the viewer (e.g. that can be serialized and shared). export class ViewerState { - dataUrl: string; - curTime: number; - cameraPosition: Vector3; - cameraTarget: Vector3; - - constructor( - dataUrl: string = DEFAULT_ZARR_URL, - curTime: number = 0, - // Default position and target from interacting with ZSNS001. - cameraPosition: Vector3 = new Vector3(500, 500, -1250), - cameraTarget: Vector3 = new Vector3(500, 500, 250), - ) { - this.dataUrl = dataUrl; - this.curTime = curTime; - this.cameraPosition = cameraPosition; - this.cameraTarget = cameraTarget; - } + dataUrl = DEFAULT_ZARR_URL; + curTime = 0; + minTime: number = -6; + maxTime: number = 5; + maxPointsPerTimepoint = 0; + pointBrightness = 1.0; + selectedPointIds: Array = []; + showTracks = true; + showTrackHighlights = true; + // Default position and target from interacting with ZSNS001. + cameraPosition = [500, 500, -1250]; + cameraTarget = [500, 500, 250]; toUrlHash(): string { - // Use SearchParams to sanitize serialized string values for URL. + // Use URLSearchParams to sanitize serialized string values for URL. const searchParams = new URLSearchParams(); searchParams.append(HASH_KEY, JSON.stringify(this)); - return searchParams.toString(); + return "#" + searchParams.toString(); } static fromUrlHash(urlHash: string): ViewerState { console.debug("getting state from hash: %s", urlHash); - // Remove the # from the hash to get the fragment. + const state = new ViewerState(); + // Remove the # from the hash to get the fragment, which + // is encoded using URLSearchParams to handle special characters. const searchParams = new URLSearchParams(urlHash.slice(1)); if (searchParams.has(HASH_KEY)) { return JSON.parse(searchParams.get(HASH_KEY)!); - } - if (urlHash.length > 0) { + } else if (urlHash.length > 0) { console.error("failed to find state key in hash: %s", urlHash); } - return new ViewerState(); + return state; } }