Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Include selection and track highlight state in shareable URL #61

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions __tests__/ViewerState.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { expect, test } from "vitest";

import { ViewerState } from "../src/lib/ViewerState";
import { Vector3 } from "three";

test("(de)serialize ViewerState", () => {
const state = new ViewerState(
"https://test.com/data/tracks.zarr", // dataUrl
5, // curTime
new Vector3(-0.5, 1, 2.5), // cameraPosition
new Vector3(1, 2, 3), // cameraTarget
new Map([[1, [2, 3, 4]]]), // selectedPoints
);

const hash = state.toUrlHash();
const revivedState = ViewerState.fromUrlHash(hash);

expect(revivedState).toEqual(state);
});
10 changes: 9 additions & 1 deletion __tests__/main.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,27 @@ import { expect, test } from "vitest";

import { PointCanvas } from "../src/lib/PointCanvas.ts";
import Scene from "../src/components/scene";
import { ViewerState } from "../src/lib/ViewerState.ts";
import React from "react";
import { render } from "@testing-library/react";
import { PointsCollection } from "../src/lib/PointSelectionBox.ts";

test("tests work", () => {
expect(true).toBeTruthy();
});

test("render Scene", () => {
let canvasState: PointCanvas | null = null;
const viewerState = new ViewerState();
const setCanvas = (canvas: PointCanvas) => {
canvasState = canvas;
};
const { container } = render(<Scene setCanvas={setCanvas} loading={false} />);
const setSelectedPoints = (selectedPoints: PointsCollection) => {};
const { container } = render(<Scene
setCanvas={setCanvas}
setSelectedPoints={setSelectedPoints}
loading={false}
initialViewerState={viewerState}/>);
expect(container).not.toBeNull();
expect(canvasState).not.toBeNull();
});
33 changes: 24 additions & 9 deletions src/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ import Scene from "@/components/scene.tsx";
import DataControls from "@/components/dataControls.tsx";
import PlaybackControls from "@/components/playbackControls.tsx";

import useSelectionBox from "@/hooks/useSelectionBox";

import { ViewerState, clearUrlHash } from "@/lib/ViewerState";
import { TrackManager, loadTrackManager } from "@/lib/TrackManager";
import { PointCanvas } from "@/lib/PointCanvas";
import { PointsCollection } from "@/lib/PointSelectionBox";

// 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();

export default function App() {
Expand All @@ -29,8 +28,11 @@ export default function App() {
const [canvas, setCanvas] = useState<PointCanvas | null>(null);
const [loading, setLoading] = useState(false);

const { selectedPoints } = useSelectionBox(canvas);
const [trackHighlightLength, setTrackHighlightLength] = useState(11);
// TODO: this should take initialViewerState.selectedPoints as its default
// value, but it we do that then react won't detect a change and won't
// fetch the corresponding tracks data.
const [selectedPoints, setSelectedPoints] = useState<PointsCollection>(new Map());
const [trackHighlightLength, setTrackHighlightLength] = useState(initialViewerState.trackHighlightLength);

// playback state
const [autoRotate, setAutoRotate] = useState(false);
Expand All @@ -42,8 +44,14 @@ export default function App() {
const copyShareableUrlToClipboard = () => {
if (canvas === null) return;
console.log("copy shareable URL to clipboard");
const state = new ViewerState(dataUrl, curTime, canvas.camera.position, canvas.controls.target);
const url = window.location.toString() + "#" + state.toUrlHash();
const state = new ViewerState(
dataUrl,
curTime,
canvas.camera.position,
canvas.controls.target,
selectedPoints,
trackHighlightLength);
const url = window.location.toString() + state.toUrlHash();
navigator.clipboard.writeText(url);
};

Expand All @@ -53,6 +61,8 @@ export default function App() {
setDataUrl(state.dataUrl);
setCurTime(state.curTime);
canvas?.setCameraProperties(state.cameraPosition, state.cameraTarget);
canvas?.selection.setSelectedPoints(state.selectedPoints);
setTrackHighlightLength(state.trackHighlightLength);
};
// update the state when the hash changes, but only register the listener once
useEffect(() => {
Expand Down Expand Up @@ -107,6 +117,11 @@ export default function App() {
setLoading(false);
canvas.setPointsPositions(data);
canvas.resetPointColors();

// Consume the internal selection here to handle initialization
// which will update the react state, and will then fetch the
// tracks data.
canvas.selection.setSelectedPoints(canvas.selection.selectedPoints());
};
getPoints(canvas, curTime);
} else {
Expand Down Expand Up @@ -203,9 +218,9 @@ export default function App() {
/>
<Scene
setCanvas={setCanvas}
setSelectedPoints={setSelectedPoints}
loading={loading}
initialCameraPosition={initialViewerState.cameraPosition}
initialCameraTarget={initialViewerState.cameraTarget}
initialViewerState={initialViewerState}
/>
<PlaybackControls
enabled={true}
Expand Down
14 changes: 11 additions & 3 deletions src/components/scene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { useEffect, useRef } from "react";
import { PointCanvas } from "@/lib/PointCanvas";
import { LoadingIndicator } from "@czi-sds/components";
import { Box } from "@mui/material";
import { PointsCollection } from "@/lib/PointSelectionBox";
import { ViewerState } from "@/lib/ViewerState";

interface SceneProps {
setCanvas: (canvas: PointCanvas) => void;
setSelectedPoints: (selectedPoints: PointsCollection) => void;
loading: boolean;
initialCameraPosition?: THREE.Vector3;
initialCameraTarget?: THREE.Vector3;
initialViewerState: ViewerState;
}

export default function Scene(props: SceneProps) {
Expand All @@ -24,7 +26,13 @@ export default function Scene(props: SceneProps) {
useEffect(() => {
// initialize the canvas
const canvas = new PointCanvas(renderWidth, renderHeight);
canvas.setCameraProperties(props.initialCameraPosition, props.initialCameraTarget);
// TODO: pass these through directly to PointCanvas
canvas.setCameraProperties(props.initialViewerState.cameraPosition, props.initialViewerState.cameraTarget);
// TODO: this seems fragile.
// We currently set selection before the changed callback to avoid
// react state change side effects (which will fetch tracks prematurely).
canvas.selection.setSelectedPoints(props.initialViewerState.selectedPoints);
canvas.selection.selectionChanged = props.setSelectedPoints;

// store the canvas in the parent component
// TODO: move this hook to the parent component?
Expand Down
104 changes: 0 additions & 104 deletions src/hooks/useSelectionBox.ts

This file was deleted.

11 changes: 8 additions & 3 deletions src/lib/PointCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { OutputPass } from "three/addons/postprocessing/OutputPass.js";
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";

import { Track } from "@/lib/three/Track";
import { PointSelectionBoxHelper } from "@/lib/PointSelectionBoxHelper";

type Tracks = Map<number, Track>;

Expand All @@ -33,7 +34,9 @@ export class PointCanvas {
composer: EffectComposer;
controls: OrbitControls;
bloomPass: UnrealBloomPass;
selection: PointSelectionBoxHelper;
tracks: Tracks = new Map();

// 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
Expand Down Expand Up @@ -82,6 +85,7 @@ export class PointCanvas {
// Set up controls
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.autoRotateSpeed = 1;
this.selection = new PointSelectionBoxHelper(this.scene, this.renderer, this.camera, this.controls);
}

// Use an arrow function so that each instance of the class is bound and
Expand All @@ -93,9 +97,9 @@ 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);
setCameraProperties(position: Vector3, target: Vector3) {
this.camera.position.set(position.x, position.y, position.z);
this.controls.target.set(target.x, target.y, target.z);
}

highlightPoints(points: number[]) {
Expand Down Expand Up @@ -196,6 +200,7 @@ export class PointCanvas {
}

dispose() {
this.selection.dispose();
this.renderer.dispose();
this.points.geometry.dispose();
if (Array.isArray(this.points.material)) {
Expand Down
Loading