Skip to content

Commit

Permalink
Move viewState into a hook
Browse files Browse the repository at this point in the history
  • Loading branch information
manzt committed Jul 25, 2024
1 parent 64f30c8 commit 1cc8ba7
Show file tree
Hide file tree
Showing 4 changed files with 31 additions and 40 deletions.
24 changes: 9 additions & 15 deletions src/components/Viewer.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import DeckGL from "deck.gl";
import { type Layer, OrthographicView } from "deck.gl";
import { type WritableAtom, useAtom } from "jotai";
import { useAtomValue } from "jotai";
import * as React from "react";

import { useViewState } from "@/hooks";
import type { LayerProps } from "@deck.gl/core/lib/layer";
import type { ZarrPixelSource } from "../ZarrPixelSource";
import type { ViewState } from "../state";
import { layerAtoms } from "../state";
import { fitBounds, isInterleaved } from "../utils";

Expand All @@ -28,11 +27,8 @@ function getLayerSize(props: Data) {
return { height, width, maxZoom };
}

function WrappedViewStateDeck(props: {
layers: Array<VizarrLayer | null>;
viewStateAtom: WritableAtom<ViewState | undefined, ViewState>;
}) {
const [viewState, setViewState] = useAtom(props.viewStateAtom);
function WrappedViewStateDeck(props: { layers: Array<VizarrLayer | null> }) {
const [viewState, setViewState] = useViewState();
const deckRef = React.useRef<DeckGL>(null);
const firstLayerProps = props.layers[0]?.props;

Expand All @@ -46,30 +42,28 @@ function WrappedViewStateDeck(props: {
setViewState(bounds);
}

// Enables screenshots of the canvas: https://github.com/visgl/deck.gl/issues/2200
const glOptions: WebGLContextAttributes = {
preserveDrawingBuffer: true,
};

return (
<DeckGL
ref={deckRef}
layers={props.layers}
viewState={viewState}
onViewStateChange={(e) => setViewState(e.viewState)}
views={[new OrthographicView({ id: "ortho", controller: true })]}
glOptions={glOptions}
glOptions={{
// Enables screenshots of the canvas: https://github.com/visgl/deck.gl/issues/2200
preserveDrawingBuffer: true,
}}
/>
);
}

function Viewer({ viewStateAtom }: { viewStateAtom: WritableAtom<ViewState | undefined, ViewState> }) {
function Viewer() {
const layerConstructors = useAtomValue(layerAtoms);
// @ts-expect-error - Viv types are giving up an issue
const layers: Array<VizarrLayer | null> = layerConstructors.map((layer) => {
return !layer.on ? null : new layer.Layer(layer.layerProps);
});
return <WrappedViewStateDeck viewStateAtom={viewStateAtom} layers={layers} />;
return <WrappedViewStateDeck layers={layers} />;
}

export default Viewer;
10 changes: 9 additions & 1 deletion src/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { type PrimitiveAtom, useAtom, useAtomValue } from "jotai";
import * as React from "react";

import { type SourceData, layerFamilyAtom } from "./state";
import { type SourceData, type ViewState, layerFamilyAtom } from "./state";
import { assert } from "./utils";

type WithId<T> = { id: string } & T;
type SourceDataAtom = PrimitiveAtom<WithId<SourceData>>;
type ViewStateAtom = PrimitiveAtom<ViewState | undefined>;

export const LayerContext = React.createContext<null | SourceDataAtom>(null);
export const ViewStateContext = React.createContext<null | ViewStateAtom>(null);

function useSourceAtom(): SourceDataAtom {
const sourceAtom = React.useContext(LayerContext);
Expand Down Expand Up @@ -40,3 +42,9 @@ export function useLayerValue() {
const layerAtom = useLayerAtom();
return useAtomValue(layerAtom);
}

export function useViewState() {
const viewStateAtom = React.useContext(ViewStateContext);
assert(viewStateAtom !== null, "useViewState must be used within a ViewStateContext.Provider");
return useAtom(viewStateAtom);
}
20 changes: 13 additions & 7 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import ReactDOM from "react-dom/client";
import Menu from "./components/Menu";
import Viewer from "./components/Viewer";
import "./codecs/register";
import { type ImageLayerConfig, type ViewState, addImageAtom, atomWithEffect } from "./state";
import { type ImageLayerConfig, type ViewState, addImageAtom } from "./state";
import { defer, typedEmitter } from "./utils";

export { version } from "../package.json";

import "./index.css";
import { ViewStateContext } from "./hooks";

type Events = {
viewStateChange: ViewState;
Expand All @@ -29,9 +30,14 @@ export interface VizarrViewer {
export function createViewer(element: HTMLElement, options: { menuOpen?: boolean } = {}): Promise<VizarrViewer> {
const ref = React.createRef<VizarrViewer>();
const emitter = typedEmitter<Events>();
const viewStateAtom = atomWithEffect<ViewState | undefined, ViewState>(
atom<ViewState | undefined>(undefined),
({ zoom, target }) => emitter.emit("viewStateChange", { zoom, target }),
const viewStateAtom = atom<ViewState | undefined>(undefined);
const viewStateAtomWithUrlSync = atom(
(get) => get(viewStateAtom),
(get, set, update) => {
const next = typeof update === "function" ? update(get(viewStateAtom)) : update;
next && emitter.emit("viewStateChange", { target: next.target, zoom: next.zoom });
set(viewStateAtom, next);
},
);
const { promise, resolve } = defer<VizarrViewer>();

Expand All @@ -54,10 +60,10 @@ export function createViewer(element: HTMLElement, options: { menuOpen?: boolean
}
}, []);
return (
<>
<ViewStateContext.Provider value={viewStateAtomWithUrlSync}>
<Menu open={options.menuOpen ?? true} />
<Viewer viewStateAtom={viewStateAtom} />
</>
<Viewer />
</ViewStateContext.Provider>
);
}
let root = ReactDOM.createRoot(element);
Expand Down
17 changes: 0 additions & 17 deletions src/state.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { ImageLayer, MultiscaleImageLayer } from "@hms-dbmi/viv";
import type { PrimitiveAtom, WritableAtom } from "jotai";
import { atom } from "jotai";
import { atomFamily, splitAtom, waitForAll } from "jotai/utils";
import type { Matrix4 } from "math.gl";
Expand All @@ -14,22 +13,6 @@ export interface ViewState {
target: [number, number];
}

export function atomWithEffect<Value, Update extends object, Result extends void | Promise<void> = void>(
baseAtom: WritableAtom<Value, Update | ((prev: Value) => Update), Result>,
callback: (data: Update) => void,
) {
const derivedAtom: typeof baseAtom = atom(
(get) => get(baseAtom),
(get, set, update) => {
const next = typeof update === "function" ? update(get(baseAtom)) : update;
const result = set(baseAtom, next);
callback(next);
return result;
},
);
return derivedAtom;
}

interface BaseConfig {
source: string | Readable;
axis_labels?: string[];
Expand Down

0 comments on commit 1cc8ba7

Please sign in to comment.