diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a7e1dae1..1037c00b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,6 +41,36 @@ jobs: - name: Lint React packages (react-hooks / react-compiler rules) run: pnpm lint:react + biome-lint: + runs-on: ubuntu-latest + permissions: + contents: read + # Biome lint/format gate over the library source (packages/*/src). The error + # backlog there has been cleared, so `biome ci` fails on any new error. + # noExplicitAny is configured as a non-blocking warning. Demo, scripts, and + # tests are intentionally out of scope (still carry findings), so this gate is + # narrower than `pnpm lint` (biome check .). + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + + - name: Setup pnpm + uses: pnpm/action-setup@v5 + with: + run_install: false + + - uses: actions/setup-node@v5 + with: + node-version: '24.14.1' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Biome lint/format (packages/*/src) + run: pnpm lint:biome + test: runs-on: ubuntu-latest steps: diff --git a/package.json b/package.json index 8b0efe48..03b6f711 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "test:proxy": "node scripts/cors-proxy.js", "validate:datasets:js": "node scripts/validate-datasets-js.js", "lint": "biome check .", + "lint:biome": "biome ci packages/*/src", "lint:react": "eslint \"packages/react/src/**/*.{ts,tsx}\" \"packages/vis/src/**/*.{ts,tsx}\"", "format": "biome format --write .", "format:check": "biome format .", diff --git a/packages/avivatorish/package.json b/packages/avivatorish/package.json index 0f758c55..ffae96e9 100644 --- a/packages/avivatorish/package.json +++ b/packages/avivatorish/package.json @@ -8,7 +8,7 @@ "exports": { ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.js" + "default": "./dist/index.js" } }, "files": [ diff --git a/packages/avivatorish/src/hooks.ts b/packages/avivatorish/src/hooks.ts index 7c7c6a78..c7bd1718 100644 --- a/packages/avivatorish/src/hooks.ts +++ b/packages/avivatorish/src/hooks.ts @@ -1,29 +1,28 @@ import { useEffect } from 'react'; -// import { useDropzone as useReactDropzone } from 'react-dropzone'; -import { shallow } from 'zustand/shallow'; // eslint-disable-next-line camelcase import { unstable_batchedUpdates } from 'react-dom'; - +// import { useDropzone as useReactDropzone } from 'react-dropzone'; +import { shallow } from 'zustand/shallow'; +import { COLOR_PALLETE, FILL_PIXEL_VALUE } from './constants'; import { + useChannelsStoreApi, useImageSettingsStore, + useImageSettingsStoreApi, useLoader, useMetadata, useViewerStore, useViewerStoreApi, - useChannelsStoreApi, - useImageSettingsStoreApi, // type PixelSource, } from './state'; import { buildDefaultSelection, - guessRgb, - getMultiSelectionStats, + createLoader, getBoundingCube, + getMultiSelectionStats, + guessRgb, isInterleaved, resolveRasterSource, } from './utils'; -import { COLOR_PALLETE, FILL_PIXEL_VALUE } from './constants'; -import { createLoader } from './utils'; export const useImage = (source?: { description: string; urlOrFile: string }, history?: any) => { const [use3d, toggleUse3d, toggleIsOffsetsSnackbarOn] = useViewerStore( @@ -54,9 +53,9 @@ export const useImage = (source?: { description: string; urlOrFile: string }, hi loaderErrorSnackbar: { on: true, message }, }) ); - //@ts-ignore flagging so we can get back to this + // TODO: flagging so we can get back to this let nextMeta: any; - //@ts-ignore flagging so we can get back to this + // TODO: flagging so we can get back to this let nextLoader: any; //PixelSource | PixelSource[] | null = null; if (Array.isArray(newLoader)) { const arr = newLoader as Array<{ metadata?: unknown; data?: unknown }>; diff --git a/packages/avivatorish/src/index.ts b/packages/avivatorish/src/index.ts index 4cfbf3b5..90914e49 100644 --- a/packages/avivatorish/src/index.ts +++ b/packages/avivatorish/src/index.ts @@ -1,8 +1,8 @@ -export * from './state'; -export * from './hooks'; -export * from './utils'; +export * from './channelStats'; export * from './constants'; -export * from './omeZarrMultiscales'; +export * from './hooks'; export * from './layerChannelState'; -export * from './channelStats'; +export * from './omeZarrMultiscales'; +export * from './state'; export * from './useChannelSelectionStats'; +export * from './utils'; diff --git a/packages/avivatorish/src/layerChannelState.ts b/packages/avivatorish/src/layerChannelState.ts index cfc9ba0c..c31e5b34 100644 --- a/packages/avivatorish/src/layerChannelState.ts +++ b/packages/avivatorish/src/layerChannelState.ts @@ -1,8 +1,7 @@ import { useCallback, useEffect, useMemo, useRef } from 'react'; -import { createStore } from 'zustand'; -import { useStore } from 'zustand'; -import { clampVivSelectionsToAxes } from './utils'; +import { createStore, useStore } from 'zustand'; import { COLOR_PALLETE, MAX_CHANNELS } from './constants'; +import { clampVivSelectionsToAxes } from './utils'; export type LayerChannelSelection = Partial<{ z: number; c: number; t: number }>; @@ -44,8 +43,7 @@ type AxisSizes = Partial>; * replacement for `JSON.stringify`-based `channelConfigKey` in the host). */ export function serializeChannelConfig(config: LayerChannelConfig): string { - const selections = - config.selections?.map((s) => [s.z ?? null, s.c ?? null, s.t ?? null]) ?? null; + const selections = config.selections?.map((s) => [s.z ?? null, s.c ?? null, s.t ?? null]) ?? null; return JSON.stringify({ channelIds: config.channelIds ?? null, colors: config.colors ?? null, @@ -208,6 +206,7 @@ export function useLayerChannelState({ const selectionAxisSizes = defaults?.selectionAxisSizes; const lastEmittedRef = useRef(null); + // biome-ignore lint/correctness/useExhaustiveDependencies: hydrate effect handles config/defaults updates; only layerId should re-seed the store. const mergedInitial = useMemo( () => mergeLayerChannelState(config, defaults, layerId), // eslint-disable-next-line react-hooks/exhaustive-deps -- hydrate effect handles config updates @@ -269,11 +268,7 @@ export function useLayerChannelState({ channelsVisible: [...current.channelsVisible, true], selections: [ ...current.selections, - mergeSelectionRow( - undefined, - current.selections[0] ?? fillSel, - selectionAxisSizes - ), + mergeSelectionRow(undefined, current.selections[0] ?? fillSel, selectionAxisSizes), ], }; storeRef.current.setState(nextMerged); @@ -285,7 +280,7 @@ export function useLayerChannelState({ const current = storeRef.current.getState(); if (current.channelCount <= 1 || index < 0 || index >= current.channelCount) return; - const splice = (arr: T[]) => arr.filter((_, i) => i !== index); + const splice = (arr: T[]) => arr.filter((_, i) => i !== index); const nextMerged: MergedLayerChannelState = { channelCount: current.channelCount - 1, channelIds: splice(current.channelIds), diff --git a/packages/avivatorish/src/state.tsx b/packages/avivatorish/src/state.tsx index 656a1d68..a9b6b3e4 100644 --- a/packages/avivatorish/src/state.tsx +++ b/packages/avivatorish/src/state.tsx @@ -1,5 +1,5 @@ -import { type PropsWithChildren, createContext, useContext } from 'react'; import type { loadOmeTiff, loadOmeZarr } from '@hms-dbmi/viv'; +import { createContext, type PropsWithChildren, useContext } from 'react'; import { createStore } from 'zustand'; import { useStoreWithEqualityFn } from 'zustand/traditional'; import type { EqFn, Selector, ZustandStore } from './zustandTypes'; @@ -340,10 +340,12 @@ export type Metadata = OME_TIFF['metadata'] | OME_METADATA; //export type Metadata = TiffPreviewProps["metadata"]; export const useMetadata = (): Metadata | undefined | null => { try { + // biome-ignore lint/correctness/useHookAtTopLevel: intentionally called outside a Viv context; the try/catch is the guard (see catch note). const image = useChannelsStore((store) => store.image); + // biome-ignore lint/correctness/useHookAtTopLevel: intentionally called outside a Viv context; the try/catch is the guard (see catch note). const metadata = useViewerStore((store) => store.metadata); return Array.isArray(metadata) ? metadata[image] : metadata; - } catch (e) { + } catch (_e) { // we now sometimes call this hook outside of a Viv context, so this is expected. // console.error("no metadata", e); } diff --git a/packages/avivatorish/src/useChannelSelectionStats.ts b/packages/avivatorish/src/useChannelSelectionStats.ts index 4fafb55f..e0ad2ce2 100644 --- a/packages/avivatorish/src/useChannelSelectionStats.ts +++ b/packages/avivatorish/src/useChannelSelectionStats.ts @@ -1,8 +1,8 @@ import { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { selectionStatsKey } from './channelStats'; import type { LayerChannelSelection } from './layerChannelState'; import type { SelectionRaster } from './utils'; import { getSingleSelectionStats } from './utils'; -import { selectionStatsKey } from './channelStats'; export type ChannelSelectionEntry = { domain: [number, number]; @@ -68,6 +68,7 @@ export function useChannelSelectionStats({ // Sync projection of the cache into result state, with fallback for unloaded // channels. Runs before paint so consumers never see a flash of stale data // when inputs change and cache has a hit. + // biome-ignore lint/correctness/useExhaustiveDependencies: deps are intentional stable-key strings (channelIdsKey/selectionsKey/...); see eslint-disable at the deps array. useLayoutEffect(() => { const cache = statsCacheRef.current; const statsByChannelId = new Map(); @@ -93,6 +94,7 @@ export function useChannelSelectionStats({ }, [channelIdsKey, selectionsKey, fallbackDomainsKey, loader]); // Async fetch loop for selection keys that differ from the last completed set. + // biome-ignore lint/correctness/useExhaustiveDependencies: deps are intentional stable-key strings (channelIdsKey/selectionsKey/...); see eslint-disable at the deps array. useEffect(() => { if (!loader) return; diff --git a/packages/avivatorish/src/utils.ts b/packages/avivatorish/src/utils.ts index 175005cf..0769de09 100644 --- a/packages/avivatorish/src/utils.ts +++ b/packages/avivatorish/src/utils.ts @@ -1,21 +1,20 @@ -import { useState, useEffect } from 'react'; -import { fromBlob, fromUrl } from 'geotiff'; -import { Matrix4 } from '@math.gl/core'; - import { - loadOmeZarr, + AdditiveColormap3DExtensions, + ColorPalette3DExtensions, getChannelStats, + loadOmeZarr, RENDERING_MODES, - ColorPalette3DExtensions, - AdditiveColormap3DExtensions, } from '@hms-dbmi/viv'; +import { Matrix4 } from '@math.gl/core'; +import { fromBlob, fromUrl } from 'geotiff'; +import { useEffect, useState } from 'react'; import { GLOBAL_SLIDER_DIMENSION_FIELDS } from './constants'; import type { OME_TIFF, PixelSource } from './state'; -const MAX_CHANNELS_FOR_SNACKBAR_WARNING = 40; +const _MAX_CHANNELS_FOR_SNACKBAR_WARNING = 40; -function isArray(value: unknown): value is unknown[] { +function _isArray(value: unknown): value is unknown[] { return Array.isArray(value) as boolean; } @@ -23,7 +22,7 @@ function isArray(value: unknown): value is unknown[] { * Guesses whether string URL or File is for an OME-TIFF image. * @param {string | File} urlOrFile */ -function isOmeTiff(urlOrFile: string | File) { +function _isOmeTiff(urlOrFile: string | File) { if (Array.isArray(urlOrFile)) return false; // local Zarr is array of File Objects const name = typeof urlOrFile === 'string' ? urlOrFile : urlOrFile.name; return name.includes('ome.tiff') || name.includes('ome.tif') || name.includes('.companion.ome'); @@ -46,7 +45,7 @@ function getMultiTiffFilenames(urlOrFiles: UrlOrFiles) { /** * Guesses whether string URL or File is one or multiple standard TIFF images. */ -function isMultiTiff(urlOrFiles: UrlOrFiles) { +function _isMultiTiff(urlOrFiles: UrlOrFiles) { const filenames = getMultiTiffFilenames(urlOrFiles); for (const filename of filenames) { const lowerCaseName = filename.toLowerCase(); @@ -84,7 +83,7 @@ export type VivSelection = { c: number; z: number; t: number }; /** * Guesses whether string URL or File is one or multiple standard TIFF images. */ -async function generateMultiTiffSources(urlOrFiles: UrlOrFiles) { +async function _generateMultiTiffSources(urlOrFiles: UrlOrFiles) { const multiTiffFiles = await generateMultiTiffFileArray(urlOrFiles); const sources: [VivSelection[], any][] = []; let c = 0; @@ -108,7 +107,7 @@ class UnsupportedBrowserError extends Error { } /** todo - clarify types / zarr... */ type OmeTiffImage = OME_TIFF; -async function getTotalImageCount(sources: OmeTiffImage[]) { +async function _getTotalImageCount(sources: OmeTiffImage[]) { const firstOmeTiffImage = sources[0]; const firstPixelSource = firstOmeTiffImage.data[0]; //@ts-expect-error - firstPixelSource._indexer is a private method @@ -136,11 +135,11 @@ async function getTotalImageCount(sources: OmeTiffImage[]) { return numImagesPerResolution * levels; } -function isZodError(e: unknown): e is Error & { issues: unknown } { +function _isZodError(e: unknown): e is Error & { issues: unknown } { return e instanceof Error && 'issues' in e; } -async function fetchSingleFileOmeTiffOffsets(url: string) { +async function _fetchSingleFileOmeTiffOffsets(url: string) { // No offsets for multifile OME-TIFFs if (url.includes('companion.ome')) { return undefined; @@ -160,7 +159,7 @@ async function fetchSingleFileOmeTiffOffsets(url: string) { */ export async function createLoader( urlOrFile: UrlOrFiles, - handleOffsetsNotFound: (arg0: boolean) => void, + _handleOffsetsNotFound: (arg0: boolean) => void, handleLoaderError: (msg: string | null) => void ) { // If the loader fails to load, handle the error (show an error snackbar). @@ -253,7 +252,10 @@ function zip(a: A[], b: B[]): [A, B][] { export function buildDefaultSelection({ labels, shape, -}: { labels: string[]; shape: number[] }): VivSelection[] { +}: { + labels: string[]; + shape: number[]; +}): VivSelection[] { const selection: VivSelection[] = []; const dimensions = zip(labels, shape).map(([name, size]) => ({ @@ -400,7 +402,10 @@ function narrowStats(stats: { domain: number[]; contrastLimits: number[] }): sta } { return narrowLimits(stats.domain) && narrowLimits(stats.contrastLimits); } -function clampVivSelectionToSource(selection: VivSelection, source: RasterSourceLike): VivSelection { +function clampVivSelectionToSource( + selection: VivSelection, + source: RasterSourceLike +): VivSelection { return (clampVivSelectionsToAxes( [selection], getVivSelectionAxisSizes(source.labels, source.shape) @@ -525,7 +530,10 @@ export async function getSingleSelectionStats2D({ export async function getSingleSelectionStats3D({ loader, selection, -}: { loader: LOADER; selection: VivSelection }) { +}: { + loader: LOADER; + selection: VivSelection; +}) { const lowResSource = getRasterSource(loader); const { shape, labels } = lowResSource; const sizeZ = getVivSelectionAxisSizes(labels, shape).z; @@ -620,7 +628,7 @@ export function isMobileOrTablet() { /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test( a ) || - /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( + /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test( a.substr(0, 4) ) ) @@ -666,16 +674,25 @@ export function getPhysicalSizeScalingMatrix(loader: PixelSource | any) { function getAxisSize(shape: number[], labels: string[], axis: 'x' | 'y' | 'z') { const index = labels.findIndex((label) => label.toLowerCase() === axis); - return index >= 0 ? shape[index] ?? 0 : 0; + return index >= 0 ? (shape[index] ?? 0) : 0; } export function getBoundingCube(loader: PixelSource | RasterSourceLike) { const source = getRasterSource(loader); const { shape, labels } = source; const physicalSizeScalingMatrix = getPhysicalSizeScalingMatrix(loader); - const xSlice: [number, number] = [0, physicalSizeScalingMatrix[0] * getAxisSize(shape, labels, 'x')]; - const ySlice: [number, number] = [0, physicalSizeScalingMatrix[5] * getAxisSize(shape, labels, 'y')]; - const zSlice: [number, number] = [0, physicalSizeScalingMatrix[10] * getAxisSize(shape, labels, 'z')]; + const xSlice: [number, number] = [ + 0, + physicalSizeScalingMatrix[0] * getAxisSize(shape, labels, 'x'), + ]; + const ySlice: [number, number] = [ + 0, + physicalSizeScalingMatrix[5] * getAxisSize(shape, labels, 'y'), + ]; + const zSlice: [number, number] = [ + 0, + physicalSizeScalingMatrix[10] * getAxisSize(shape, labels, 'z'), + ]; return [xSlice, ySlice, zSlice]; } diff --git a/packages/avivatorish/tsconfig.json b/packages/avivatorish/tsconfig.json index 6b4f3b1a..a3036049 100644 --- a/packages/avivatorish/tsconfig.json +++ b/packages/avivatorish/tsconfig.json @@ -13,7 +13,7 @@ "skipLibCheck": true, "esModuleInterop": true, "resolveJsonModule": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ES2022", "DOM", "DOM.Iterable"], // Cross-package types resolve via node_modules to each package's built dist/*.d.ts // (topological build order guarantees deps are built first). Matches core's pattern. "types": ["react", "react-dom"] diff --git a/packages/core/package.json b/packages/core/package.json index cdd4f72c..54f9bfd0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -8,14 +8,15 @@ "exports": { ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.js" + "import": "./dist/index.js", + "require": "./dist/index.cjs" }, "./workers": { "types": "./dist/workers/index.d.ts", - "import": "./dist/workers/index.js" + "default": "./dist/workers.js" }, "./points-worker": { - "import": "./dist/points-worker.js" + "default": "./dist/points-worker.js" } }, "files": [ diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f05c98ef..6489cc6e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -4,60 +4,64 @@ * Core library for interfacing with SpatialData stores in TypeScript/JavaScript */ -// export * from './schemas/index.js'; -export * from './types.js'; -export * from './store/index.js'; export * from './models/index.js'; -export * from './spatialViewFit.js'; -export * from './pointsTiling.js'; +export { + type GeopandasGeoParquetMetadata, + inferShapesGeometryKindFromParquet, + readGeopandasGeoParquetMetadata, +} from './models/VShapesSource.js'; +export { tableToIndexColumnName } from './models/VTableSource.js'; export { featureCodeMapFromCatalog, mergeFeatureCountsIntoCatalog, remapRowFeatureCodes, } from './pointsFeatures.js'; export { - POINTS_PRELOAD_MAX_ROWS, + applyRenderCapToColumnar, DEFAULT_POINTS_MEMORY_CAP, DEFAULT_POINTS_RENDER_CAP, - PointsPreloadTooLargeError, - applyRenderCapToColumnar, exceedsPointsPreloadLimit, - pointsPreloadTruncatedMessage, + POINTS_PRELOAD_MAX_ROWS, + PointsPreloadTooLargeError, pointsFilteredMemoryCapMessage, + pointsPreloadTruncatedMessage, preloadedColumnarPointCount, resolvePointsMemoryCap, resolvePointsRenderCap, } from './pointsLimits.js'; -export type { PointsLoadOptions, PointsLoadProgress, PointsLoadResult } from './pointsLoadOptions.js'; -export { - enablePointsWorker, - disablePointsWorker, - ensurePointsWorker, - filterColumnarByFeatureCodesInWorker, - isPointsWorkerEnabled, - setPointsWorkerDefaultEnabled, - setPointsWorkerRequestTimeout, -} from './workers/index.js'; export { + type ColumnarNdarrayPointsBatch, + type CorePointsLoader, createMortonTiledPointsLoader, createPointsLoaderForElement, createPreloadedColumnarPointsLoader, - resolvePointsEncoding, - type CorePointsLoader, - type ColumnarNdarrayPointsBatch, - type PreloadedColumnarInput, type PointsBatch, type PointsBatchFormat, type PointsEncodingKind, - type PointsLoadInBoundsOptions, type PointsLoaderCapabilities, + type PointsLoadInBoundsOptions, + type PreloadedColumnarInput, + resolvePointsEncoding, } from './pointsLoader.js'; +export type { + PointsLoadOptions, + PointsLoadProgress, + PointsLoadResult, +} from './pointsLoadOptions.js'; +export * from './pointsTiling.js'; export * from './shapes.js'; -export { - inferShapesGeometryKindFromParquet, - readGeopandasGeoParquetMetadata, - type GeopandasGeoParquetMetadata, -} from './models/VShapesSource.js'; -export { tableToIndexColumnName } from './models/VTableSource.js'; +export * from './spatialViewFit.js'; +export * from './store/index.js'; export * from './tableAssociations.js'; export * from './tooltip.js'; +// export * from './schemas/index.js'; +export * from './types.js'; +export { + disablePointsWorker, + enablePointsWorker, + ensurePointsWorker, + filterColumnarByFeatureCodesInWorker, + isPointsWorkerEnabled, + setPointsWorkerDefaultEnabled, + setPointsWorkerRequestTimeout, +} from './workers/index.js'; diff --git a/packages/core/src/models/VAnnDataSource.ts b/packages/core/src/models/VAnnDataSource.ts index 872a0023..07ea1e2a 100644 --- a/packages/core/src/models/VAnnDataSource.ts +++ b/packages/core/src/models/VAnnDataSource.ts @@ -1,8 +1,8 @@ -import { open as zarrOpen, get as zarrGet } from 'zarrita'; +import { get as zarrGet, open as zarrOpen } from 'zarrita'; +import type { TableColumnData } from '../types'; +import type { DataSourceParams } from '../Vutils'; import { dirname } from '../Vutils'; import ZarrDataSource from './VZarrDataSource'; -import type { DataSourceParams } from '../Vutils'; -import type { TableColumnData } from '../types'; function prependSlash(path: string) { if (typeof path === 'string' && path.length >= 1) { @@ -183,7 +183,7 @@ export default class AnnDataSource extends ZarrDataSource { * @returns {Promise} An promise for a zarr array * containing the indices. */ - loadObsIndex(path?: string) { + loadObsIndex(_path?: string) { if (this.obsIndex) { return this.obsIndex; } @@ -216,7 +216,7 @@ export default class AnnDataSource extends ZarrDataSource { */ loadVarIndex( // eslint-disable-next-line no-unused-vars - path = undefined + _path = undefined ) { if (this.varIndex) { return this.varIndex; @@ -233,7 +233,7 @@ export default class AnnDataSource extends ZarrDataSource { * @param {string|undefined} matrixPath * @returns {Promise} An promise for a zarr array containing the aliased names. */ - async loadVarAlias(varPath: string, matrixPath?: string) { + async loadVarAlias(varPath: string, _matrixPath?: string) { if (this.varAlias) { return this.varAlias; } @@ -348,7 +348,7 @@ export default class AnnDataSource extends ZarrDataSource { keys.map(async (key) => { try { result[key] = await this._loadElement(`${path}/${key}`); - } catch (e) { + } catch (_e) { console.error(`Error in _loadDict: could not load ${key}`); result[key] = undefined; //not sure how useful this is } diff --git a/packages/core/src/models/VPointsSource.ts b/packages/core/src/models/VPointsSource.ts index 0d0fc7c0..e175e1dc 100644 --- a/packages/core/src/models/VPointsSource.ts +++ b/packages/core/src/models/VPointsSource.ts @@ -1,10 +1,17 @@ -import { basename } from '../Vutils'; +import { decodeIntStat, parseParquetFileMetaData } from '../parquetFooterStats.js'; import { buildFeatureCatalogFromColumns, featureCodeMapFromCatalog, mergeFeatureCountsIntoCatalog, resolveRowFeatureCodesFromTable, } from '../pointsFeatures.js'; +import { exceedsPointsPreloadLimit, resolvePointsMemoryCap } from '../pointsLimits.js'; +import type { + PointsLoadOptions, + PointsLoadProgress, + PointsLoadResult, +} from '../pointsLoadOptions.js'; +import { basename } from '../Vutils'; import { decodeGeometryWithFeaturesInWorker, decodeParquetGeometryCappedInWorker, @@ -16,16 +23,6 @@ import { scanParquetFeatureCatalogInWorker, scanParquetFeatureCountsInWorker, } from '../workers/pointsWorkerClient.js'; -import { exceedsPointsPreloadLimit, resolvePointsMemoryCap } from '../pointsLimits.js'; -import { - decodeIntStat, - parseParquetFileMetaData, -} from '../parquetFooterStats.js'; -import type { - PointsLoadOptions, - PointsLoadProgress, - PointsLoadResult, -} from '../pointsLoadOptions.js'; interface ColumnarPointsChunk { shape: number[]; @@ -62,7 +59,7 @@ function rowGroupFeatureCodeExtents( return []; } const metaBytes = part.schemaBytes.subarray(0, part.schemaBytes.length - 8); - let footer; + let footer: ReturnType; try { footer = parseParquetFileMetaData(metaBytes); } catch { @@ -207,16 +204,17 @@ function pointsScanChunkProgress( }, }; } + import { - MORTON_CODE_2D_COLUMN, - type PointsInBoundsOptions, - type PointsInBoundsResponse, - type PointsFeatureCatalog, - type PointsTilingMetadata, extractSentinelBoundingBox, featureCodeAllowSet, filterPointsToBounds, + MORTON_CODE_2D_COLUMN, mortonIntervalsForBounds, + type PointsFeatureCatalog, + type PointsInBoundsOptions, + type PointsInBoundsResponse, + type PointsTilingMetadata, } from '../pointsTiling.js'; import type { Axis } from '../schemas'; // import { normalizeAxes } from '@vitessce/spatial-utils'; @@ -517,7 +515,9 @@ export default class SpatialDataPointsSource extends SpatialDataTableSource { if (featureKey) { const nameColumn = arrowTable.getChild(featureKey); if (nameColumn) { - const codeColumn = featureCodeColumnName ? arrowTable.getChild(featureCodeColumnName) : null; + const codeColumn = featureCodeColumnName + ? arrowTable.getChild(featureCodeColumnName) + : null; featureCatalog = buildFeatureCatalogFromColumns( featureKey, nameColumn, @@ -565,7 +565,7 @@ export default class SpatialDataPointsSource extends SpatialDataTableSource { return parts.length > 0 ? { parts } : null; } - async* loadPointsMatchingFeatureCodesByChunk( + async *loadPointsMatchingFeatureCodesByChunk( elementPath: string, options: { memoryCap: number; @@ -578,7 +578,7 @@ export default class SpatialDataPointsSource extends SpatialDataTableSource { * code space the selection was made in. When absent for a dict-only * element the scan cannot match by name and returns nothing. */ featureCodeByName?: ReadonlyMap; - } + } ) { ensurePointsWorker(); const parquetPath = getParquetPath(elementPath); @@ -610,8 +610,8 @@ export default class SpatialDataPointsSource extends SpatialDataTableSource { const schemaTable = datasetMetadata ? null : await this.loadParquetSchemaTable(parquetPath); const fields = datasetMetadata?.schema?.fields ? datasetMetadata.schema.fields.flatMap((field) => - typeof field.name === 'string' ? [field.name] : [] - ) + typeof field.name === 'string' ? [field.name] : [] + ) : arrowSchemaFieldNames(schemaTable); const featureCodeColumnName = selectFeatureCodeColumn(fields, featureKey); @@ -657,10 +657,10 @@ export default class SpatialDataPointsSource extends SpatialDataTableSource { const rowGroupExtents = featureCodeColumnName && datasetMetadata ? rowGroupFeatureCodeExtents( - datasetMetadata.parts, - featureCodeColumnName, - datasetRowGroups - ) + datasetMetadata.parts, + featureCodeColumnName, + datasetRowGroups + ) : []; const canSkipRowGroups = rowGroupExtents.length === datasetRowGroups; @@ -670,11 +670,7 @@ export default class SpatialDataPointsSource extends SpatialDataTableSource { } if ( canSkipRowGroups && - !extentMayContainSelectedCodes( - rowGroupExtents[rowGroupIndex], - selectedMin, - selectedMax - ) + !extentMayContainSelectedCodes(rowGroupExtents[rowGroupIndex], selectedMin, selectedMax) ) { continue; } @@ -1100,9 +1096,7 @@ export default class SpatialDataPointsSource extends SpatialDataTableSource { }); const catalog = await scanParquetFeatureCatalogInWorker({ rowGroups: - featureCodeColumnName && payload.rowGroups.length > 0 - ? payload.rowGroups - : undefined, + featureCodeColumnName && payload.rowGroups.length > 0 ? payload.rowGroups : undefined, parts: payload.parts, columns: columnNames, featureKey, @@ -1120,8 +1114,11 @@ export default class SpatialDataPointsSource extends SpatialDataTableSource { } } - const { accumulateFeatureCatalogFromTable, featureCatalogFromCodeMap, featureCatalogNeedsParquetFallback } = - await import('../pointsFeatures.js'); + const { + accumulateFeatureCatalogFromTable, + featureCatalogFromCodeMap, + featureCatalogNeedsParquetFallback, + } = await import('../pointsFeatures.js'); const codeToName = new Map(); const nameToCode = new Map(); const canUseRowGroups = await this.canLoadParquetRowGroups(); @@ -1179,7 +1176,7 @@ export default class SpatialDataPointsSource extends SpatialDataTableSource { if (this.pointTilingMetadataCache.has(elementPath)) { return this.pointTilingMetadataCache.get(elementPath) ?? null; } - const promise = this.loadPointsTilingMetadataUncached(elementPath).catch(error => { + const promise = this.loadPointsTilingMetadataUncached(elementPath).catch((error) => { this.pointTilingMetadataCache.delete(elementPath); throw error; }); diff --git a/packages/core/src/models/VShapesSource.ts b/packages/core/src/models/VShapesSource.ts index e5bf115e..3214b630 100644 --- a/packages/core/src/models/VShapesSource.ts +++ b/packages/core/src/models/VShapesSource.ts @@ -18,6 +18,7 @@ import WKB from 'ol/format/WKB.js'; import { basename } from '../Vutils'; + // import { log } from '@vitessce/globals'; const log = console; diff --git a/packages/core/src/models/VTableSource.ts b/packages/core/src/models/VTableSource.ts index 3e3116f5..a0f970dc 100644 --- a/packages/core/src/models/VTableSource.ts +++ b/packages/core/src/models/VTableSource.ts @@ -1,7 +1,6 @@ // this is a direct copy of the Vitessce implementation, with changes mostly to make it more normal TypeScript. import { type Table as ArrowTable, tableFromIPC } from 'apache-arrow'; -import type { DataSourceParams } from '../Vutils'; import { getParquetModule, type ParquetModule, @@ -9,6 +8,7 @@ import { type ParquetWasmMetadata, } from '../parquetWasmLoader.js'; import type { TableColumnData } from '../types'; +import type { DataSourceParams } from '../Vutils'; import AnnDataSource from './VAnnDataSource'; export type { ParquetRowGroupReadOptions }; @@ -435,7 +435,7 @@ export default class SpatialDataTableSource extends AnnDataSource { } protected async resolveParquetRowCount(parquetPath: string): Promise { - // may be better to cache this? we get e.g. a lot of 404 requests for `points.parquet/points.4.parquet` + // may be better to cache this? we get e.g. a lot of 404 requests for `points.parquet/points.4.parquet` const datasetMetadata = await this.loadParquetDatasetMetadata(parquetPath); if (datasetMetadata?.totalNumRows) { return datasetMetadata.totalNumRows; @@ -654,9 +654,7 @@ export default class SpatialDataTableSource extends AnnDataSource { const rowGroups = canUseRowGroups ? await this.readParquetRowGroupsBytesCapped(parquetPath, options.maxRows) : []; - const partsMaxRows = options.fullPartsForFallback - ? Number.POSITIVE_INFINITY - : options.maxRows; + const partsMaxRows = options.fullPartsForFallback ? Number.POSITIVE_INFINITY : options.maxRows; const { parts } = await this.readParquetDatasetBytesCapped(parquetPath, partsMaxRows); return { rowGroups, parts }; } @@ -700,11 +698,10 @@ export default class SpatialDataTableSource extends AnnDataSource { return null; } const columnOptions: ParquetRowGroupReadOptions = { columns: [columnName] }; - const minTable = await this.loadParquetRowGroupByGroupIndex( - parquetPath, - rowGroupIndex, - { ...columnOptions, limit: 1 } - ); + const minTable = await this.loadParquetRowGroupByGroupIndex(parquetPath, rowGroupIndex, { + ...columnOptions, + limit: 1, + }); const minColumn = minTable?.getChild(columnName); if (!minColumn || minColumn.length === 0) { return null; @@ -891,11 +888,7 @@ export default class SpatialDataTableSource extends AnnDataSource { if (options.useRowGroupReads === true && (await this.canLoadParquetRowGroups())) { try { - const table = await this._loadParquetTableRowGroupsCapped( - parquetPath, - columns, - targetRows - ); + const table = await this._loadParquetTableRowGroupsCapped(parquetPath, columns, targetRows); return { table, totalRows, truncated }; } catch (error) { console.warn( @@ -1075,11 +1068,7 @@ export default class SpatialDataTableSource extends AnnDataSource { let resolvedColumns = columns; if (columns?.length) { - resolvedColumns = await this.resolveParquetTableColumns( - parquetPath, - columns, - readSchema - ); + resolvedColumns = await this.resolveParquetTableColumns(parquetPath, columns, readSchema); const wasmSchema = readSchema(normalizedBytes); const arrowTableForSchema = await tableFromIPC(wasmSchema.intoIPCStream()); const indexColumnName = tableToIndexColumnName(arrowTableForSchema); @@ -1138,13 +1127,7 @@ export default class SpatialDataTableSource extends AnnDataSource { const dataset = await this.loadParquetDatasetMetadata(parquetPath); if (dataset && dataset.parts.length > 1) { - return this.loadMultipartParquetTable( - parquetPath, - columns, - dataset, - readParquet, - readSchema - ); + return this.loadMultipartParquetTable(parquetPath, columns, dataset, readParquet, readSchema); } const partPaths = await this.discoverMultipartPartPaths(parquetPath); @@ -1158,7 +1141,7 @@ export default class SpatialDataTableSource extends AnnDataSource { ); } - let parquetBytes = await this.loadParquetBytes(parquetPath); + const parquetBytes = await this.loadParquetBytes(parquetPath); if (!parquetBytes) { throw new Error('Failed to load parquet data from store.'); } diff --git a/packages/core/src/models/VZarrDataSource.ts b/packages/core/src/models/VZarrDataSource.ts index f8e06ba7..9c37494c 100644 --- a/packages/core/src/models/VZarrDataSource.ts +++ b/packages/core/src/models/VZarrDataSource.ts @@ -1,6 +1,7 @@ // import { zarrOpenRoot } from '@vitessce/zarr-utils'; -import { open as zarrOpen, root as zarrRoot } from 'zarrita'; + import type { Group, Location, Readable } from 'zarrita'; +import { open as zarrOpen, root as zarrRoot } from 'zarrita'; import type { DataSourceParams } from '../Vutils'; /** @@ -12,13 +13,7 @@ export default class ZarrDataSource { /** * @param params The parameters object. */ - constructor({ - url, - requestInit, - refSpecUrl, - store, - fileType, - }: DataSourceParams & { refSpecUrl?: string }) { + constructor({ url, store }: DataSourceParams & { refSpecUrl?: string }) { console.info( 'Using a Zarr-based data source. 403 and 404 HTTP responses for Zarr metadata files (.zattrs, .zarray, .zgroup, zarr.json) are to be expected and do not necessarily indicate errors.' ); diff --git a/packages/core/src/models/index.ts b/packages/core/src/models/index.ts index 915eba9b..53e1d4eb 100644 --- a/packages/core/src/models/index.ts +++ b/packages/core/src/models/index.ts @@ -1,18 +1,19 @@ import * as ad from 'anndata.js'; import { createPrefixedStore } from 'zarrextra'; import type * as zarr from 'zarrita'; +import type { PointsLoadOptions, PointsLoadProgress } from '../pointsLoadOptions.js'; +import type { PointsFeatureCatalog } from '../pointsTiling.js'; import { type CoordinateTransformation, type PointsAttrs, - type RasterAttrs, - type ShapesAttrs, - type TableAttrs, pointsAttrsSchema, + type RasterAttrs, rasterAttrsSchema, + type ShapesAttrs, shapesAttrsSchema, + type TableAttrs, tableAttrsSchema, } from '../schemas'; -import type { PointsLoadOptions, PointsLoadProgress } from '../pointsLoadOptions.js'; import type { ShapesRenderData } from '../shapes'; import { isSpatialData, loadFeatureRowIndexByFeatureIndex } from '../tableAssociations'; import { type BaseTransformation, Identity, parseTransforms } from '../transformations'; @@ -26,12 +27,10 @@ import type { ZAttrsAny, ZarrTree, } from '../types'; -import { ATTRS_KEY } from '../types'; -import { Err, Ok } from '../types'; +import { ATTRS_KEY, Err, Ok } from '../types'; import SpatialDataPointsSource from './VPointsSource'; import SpatialDataShapesSource from './VShapesSource'; import SpatialDataTableSource from './VTableSource'; -import type { PointsFeatureCatalog } from '../pointsTiling.js'; /** * Parameters for creating element instances. @@ -682,4 +681,4 @@ export function loadElements( } // Re-export types that may be useful externally -export type { RasterAttrs, ShapesAttrs, PointsAttrs, CoordinateTransformation }; +export type { CoordinateTransformation, PointsAttrs, RasterAttrs, ShapesAttrs }; diff --git a/packages/core/src/parquetFooterStats.ts b/packages/core/src/parquetFooterStats.ts index 481fb8e2..1cbb184b 100644 --- a/packages/core/src/parquetFooterStats.ts +++ b/packages/core/src/parquetFooterStats.ts @@ -330,7 +330,10 @@ export function parseParquetFileMetaData(fileMetaDataBytes: Uint8Array): Parquet } /** Decode a `Statistics` min/max value for an integer physical type (little-endian). */ -export function decodeIntStat(bytes: Uint8Array | undefined, physicalType: number | null): number | null { +export function decodeIntStat( + bytes: Uint8Array | undefined, + physicalType: number | null +): number | null { if (!bytes || bytes.length === 0) return null; const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); if (physicalType === ParquetPhysicalType.INT32) { diff --git a/packages/core/src/parquetWasmLoader.ts b/packages/core/src/parquetWasmLoader.ts index 9252738c..9082317a 100644 --- a/packages/core/src/parquetWasmLoader.ts +++ b/packages/core/src/parquetWasmLoader.ts @@ -71,7 +71,19 @@ async function initializeParquetModule(module: unknown) { // Vitest/Node load the vendored browser ESM glue; initialize WASM from disk // because undici cannot fetch file:// URLs. - if (import.meta.url.startsWith('file:') && typeof initSync === 'function') { + // + // Detect Node from the runtime itself, NOT from `import.meta.url`. Bundlers + // rewrite `import.meta.url` to a file:// URL inside a *browser* bundle + // (webpack does this for the docs site), so a `startsWith('file:')` check is + // true in the browser too. That sent us into this branch with node:fs/node:url/ + // node:path replaced by empty browser stubs, so `fileURLToPath` was undefined: + // TypeError: t is not a function + const isNodeRuntime = + typeof process !== 'undefined' && + process.versions?.node != null && + typeof window === 'undefined'; + + if (isNodeRuntime && typeof initSync === 'function') { const [{ readFileSync }, { fileURLToPath }, { dirname, join }] = await Promise.all([ import('node:fs'), import('node:url'), diff --git a/packages/core/src/pointsFeatures.ts b/packages/core/src/pointsFeatures.ts index f555191e..68d310fb 100644 --- a/packages/core/src/pointsFeatures.ts +++ b/packages/core/src/pointsFeatures.ts @@ -1,5 +1,5 @@ -import { Type } from 'apache-arrow'; import type { Table, Vector } from 'apache-arrow'; +import { Type } from 'apache-arrow'; import { isMortonSentinelValue, MORTON_CODE_2D_COLUMN, @@ -259,7 +259,7 @@ function getDictionaryIndexAt(column: Vector, row: number): number | null { return null; } -function dictionaryIndexArray(column: Vector, numRows: number): Int32Array | null { +function _dictionaryIndexArray(column: Vector, numRows: number): Int32Array | null { if (!isDictionaryFeatureColumn(column)) { return null; } diff --git a/packages/core/src/pointsLimits.ts b/packages/core/src/pointsLimits.ts index 91260e47..1b176860 100644 --- a/packages/core/src/pointsLimits.ts +++ b/packages/core/src/pointsLimits.ts @@ -137,8 +137,6 @@ export function pointsFilteredMemoryCapMessage( scannedRows?: number ): string { const scanned = - scannedRows !== undefined - ? ` after scanning ${scannedRows.toLocaleString()} rows` - : ''; + scannedRows !== undefined ? ` after scanning ${scannedRows.toLocaleString()} rows` : ''; return `Showing ${loadedCount.toLocaleString()} matching points (memory cap ${memoryCap.toLocaleString()}${scanned})`; } diff --git a/packages/core/src/pointsLoader.ts b/packages/core/src/pointsLoader.ts index 4a3f5ef6..144f5222 100644 --- a/packages/core/src/pointsLoader.ts +++ b/packages/core/src/pointsLoader.ts @@ -1,10 +1,10 @@ import type { PointsElement } from './models/index.js'; -import type { PointsLoadMode } from './types.js'; import type { PointsInBoundsResponse, PointsTilingMetadata, SpatialBounds, } from './pointsTiling.js'; +import type { PointsLoadMode } from './types.js'; export type PointsEncodingKind = | 'preloaded-columnar' diff --git a/packages/core/src/schemas/index.ts b/packages/core/src/schemas/index.ts index 0f4408ac..a7aca93c 100644 --- a/packages/core/src/schemas/index.ts +++ b/packages/core/src/schemas/index.ts @@ -323,10 +323,9 @@ export type NgffImage = z.infer; * and does NOT control OME-NGFF format detection (which is determined by structure). * - For shapes/points: `version` is the spatialdata format version (e.g., '0.1', '0.2') and IS used for format detection. */ -export const spatialDataAttrsSchema = z - .looseObject({ - version: z.string(), - }); // allow extra fields we don't validate yet +export const spatialDataAttrsSchema = z.looseObject({ + version: z.string(), +}); // allow extra fields we don't validate yet export type SpatialDataAttrs = z.infer; @@ -334,8 +333,34 @@ export type SpatialDataAttrs = z.infer; * Schema for raster element attrs in spatialdata 0.5.0 format * Uses OME-NGFF 0.4 format with multiscales at the top level */ -const rasterAttrs_OME_04_Schema = z - .looseObject({ +const rasterAttrs_OME_04_Schema = z.looseObject({ + multiscales: z + .array( + z.object({ + name: z.string().optional(), + datasets: z + .array( + z.object({ + path: z.string(), + coordinateTransformations: coordinateTransformationSchema.optional(), + }) + ) + .min(1), + axes: axesSchema, + coordinateTransformations: coordinateTransformationSchema.optional(), + }) + ) + .min(1), + omero: omeroSchema.optional(), + spatialdata_attrs: spatialDataAttrsSchema.optional(), +}); + +/** + * Schema for raster element attrs in spatialdata 0.6.1+ format + * Uses OME-NGFF 0.5 format with multiscales nested under 'ome' key + */ +const rasterAttrs_OME_05_Schema = z.looseObject({ + ome: z.looseObject({ multiscales: z .array( z.object({ @@ -354,38 +379,9 @@ const rasterAttrs_OME_04_Schema = z ) .min(1), omero: omeroSchema.optional(), - spatialdata_attrs: spatialDataAttrsSchema.optional(), - }); - -/** - * Schema for raster element attrs in spatialdata 0.6.1+ format - * Uses OME-NGFF 0.5 format with multiscales nested under 'ome' key - */ -const rasterAttrs_OME_05_Schema = z - .looseObject({ - ome: z - .looseObject({ - multiscales: z - .array( - z.object({ - name: z.string().optional(), - datasets: z - .array( - z.object({ - path: z.string(), - coordinateTransformations: coordinateTransformationSchema.optional(), - }) - ) - .min(1), - axes: axesSchema, - coordinateTransformations: coordinateTransformationSchema.optional(), - }) - ) - .min(1), - omero: omeroSchema.optional(), - }), - spatialdata_attrs: spatialDataAttrsSchema.optional(), - }); + }), + spatialdata_attrs: spatialDataAttrsSchema.optional(), +}); /** * Schema for raster element attrs (images & labels) @@ -446,13 +442,12 @@ export type RasterAttrs = { * Schema for shapes element attrs. * Transformations are at the top level with input/output coordinate system references. */ -export const shapesAttrsSchema = z - .looseObject({ - 'encoding-type': z.string().optional(), // e.g., 'ngff:shapes' - axes: z.array(z.string()).optional(), // e.g., ['x', 'y'] - coordinateTransformations: coordinateTransformationSchema.optional(), - spatialdata_attrs: spatialDataAttrsSchema.optional(), - }); +export const shapesAttrsSchema = z.looseObject({ + 'encoding-type': z.string().optional(), // e.g., 'ngff:shapes' + axes: z.array(z.string()).optional(), // e.g., ['x', 'y'] + coordinateTransformations: coordinateTransformationSchema.optional(), + spatialdata_attrs: spatialDataAttrsSchema.optional(), +}); export type ShapesAttrs = z.infer; @@ -460,26 +455,27 @@ export type ShapesAttrs = z.infer; * Schema for points element attrs. * Transformations are at the top level with input/output coordinate system references. */ -export const pointsAttrsSchema = z - .looseObject({ - 'encoding-type': z.string().optional(), // e.g., 'ngff:points' - axes: z.array(z.string()).optional(), // e.g., ['x', 'y'] - coordinateTransformations: coordinateTransformationSchema.optional(), - spatialdata_attrs: spatialDataAttrsSchema.optional(), - }); +export const pointsAttrsSchema = z.looseObject({ + 'encoding-type': z.string().optional(), // e.g., 'ngff:points' + axes: z.array(z.string()).optional(), // e.g., ['x', 'y'] + coordinateTransformations: coordinateTransformationSchema.optional(), + spatialdata_attrs: spatialDataAttrsSchema.optional(), +}); export type PointsAttrs = z.infer; /** * Schema for anndata table metadata */ -export const tableAttrsSchema = z - .looseObject({ - instance_key: z.string().optional().nullable(), - region: z.union([z.string(), z.array(z.string())]).optional().nullable(), - region_key: z.string().optional().nullable(), - 'spatialdata-encoding-type': z.literal('ngff:regions_table'), - }); +export const tableAttrsSchema = z.looseObject({ + instance_key: z.string().optional().nullable(), + region: z + .union([z.string(), z.array(z.string())]) + .optional() + .nullable(), + region_key: z.string().optional().nullable(), + 'spatialdata-encoding-type': z.literal('ngff:regions_table'), +}); export type TableAttrs = z.infer; diff --git a/packages/core/src/store/index.ts b/packages/core/src/store/index.ts index 1fd26c32..8c4cbc8f 100644 --- a/packages/core/src/store/index.ts +++ b/packages/core/src/store/index.ts @@ -2,27 +2,16 @@ * Store interface for reading SpatialData from zarr stores */ -import type * as zarr from 'zarrita'; -import { getTransformation } from '../transformations'; -import { - type ConsolidatedStore, - openExtraConsolidated, - serializeZarrTree, -} from 'zarrextra'; +import { type ConsolidatedStore, openExtraConsolidated, serializeZarrTree } from 'zarrextra'; import { - loadElements, type ElementInstanceMap, + loadElements, type SpatialElement, type TableElement, } from '../models'; -import type { - ElementName, - StoreLocation, - StoreReference, - BadFileHandler, - ZarrTree, -} from '../types'; -import { SpatialElementNames, ElementNames } from '../types'; +import { getTransformation } from '../transformations'; +import type { BadFileHandler, ElementName, StoreLocation, StoreReference } from '../types'; +import { ElementNames, SpatialElementNames } from '../types'; /** * Type alias for element collections - maps element keys to element instances @@ -34,7 +23,7 @@ function elementPathCandidates(kind: Exclude, key: string } // Re-export SpatialElement from models -export type { SpatialElement, AnyElement } from '../models'; +export type { AnyElement, SpatialElement } from '../models'; function describeStoreSource(url?: StoreLocation): string { return url ?? '[store instance]'; diff --git a/packages/core/src/tooltip.ts b/packages/core/src/tooltip.ts index c0e7e5ed..40ee8e20 100644 --- a/packages/core/src/tooltip.ts +++ b/packages/core/src/tooltip.ts @@ -1,10 +1,10 @@ import type { LabelsElement, ShapesElement } from './models'; import type { SpatialData } from './store'; -import type { TableColumnData } from './types'; import { loadAssociatedTableFeatureRows, loadFeatureRowIndexByFeatureIndex, } from './tableAssociations'; +import type { TableColumnData } from './types'; export type SpatialFeatureTooltipItem = { label: string; @@ -233,7 +233,11 @@ export async function loadShapesTooltipMetadata( associated.rowIds.every((rowId, index) => rowId === featureIds[index]); if (!isDirectlyAligned) { - tooltipRowIndices = await loadShapesRowIndexByFeatureIndex(spatialData, element.key, featureIds); + tooltipRowIndices = await loadShapesRowIndexByFeatureIndex( + spatialData, + element.key, + featureIds + ); } } diff --git a/packages/core/src/transformations/index.ts b/packages/core/src/transformations/index.ts index 4c9fdf91..e8802fb5 100644 --- a/packages/core/src/transformations/index.ts +++ b/packages/core/src/transformations/index.ts @@ -1,2 +1,2 @@ -export * from './transformations.js'; export * from './operations.js'; +export * from './transformations.js'; diff --git a/packages/core/src/transformations/operations.ts b/packages/core/src/transformations/operations.ts index 11f583ec..fea2e293 100644 --- a/packages/core/src/transformations/operations.ts +++ b/packages/core/src/transformations/operations.ts @@ -1,8 +1,8 @@ import type { Matrix4 } from '@math.gl/core'; -import type { SpatialElement, CoordinateSystemNotFoundError } from '../models'; -import type { BaseTransformation } from './transformations'; +import type { CoordinateSystemNotFoundError, SpatialElement } from '../models'; import type { Result } from '../types'; import { unwrap } from '../types'; +import type { BaseTransformation } from './transformations'; export type MappingToCoordinateSystem_t = Map; diff --git a/packages/core/src/transformations/transformations.ts b/packages/core/src/transformations/transformations.ts index 70798e8f..fbb68e37 100644 --- a/packages/core/src/transformations/transformations.ts +++ b/packages/core/src/transformations/transformations.ts @@ -1,5 +1,5 @@ import { Matrix4 } from '@math.gl/core'; -import type { CoordinateTransformation, Axis } from '../schemas'; +import type { Axis, CoordinateTransformation } from '../schemas'; /** * Coordinate system reference from NGFF transformations. @@ -172,7 +172,7 @@ function composeMatricesInApplicationOrder(matrices: Matrix4[]): Matrix4 { // Matrix products apply right-to-left to points, so we multiply in reverse // to preserve the listed/application order of transformations. for (let i = matrices.length - 1; i >= 0; i--) { - matrix.multiplyRight(matrices[i]!); + matrix.multiplyRight(matrices[i]); } return matrix; } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index a515fad5..1c6b9f8c 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -13,8 +13,8 @@ */ import type * as ad from 'anndata.js'; -import type * as zarr from 'zarrita'; import type { ConsolidatedStore } from 'zarrextra'; +import type * as zarr from 'zarrita'; /** * Element name constants and types @@ -76,13 +76,10 @@ export type ZGroup = zarr.Group; // Re-export zarr-related types from zarrextra for convenience // These are used in SDataProps and models, so we keep them accessible from core/types -export type { ZarrTree, LazyZarrArray, ZAttrsAny } from 'zarrextra'; -export { ATTRS_KEY, ZARRAY_KEY } from 'zarrextra'; - // Re-export Result type and utilities from zarrextra for convenience // Result is used throughout core for explicit error handling -export type { Result } from 'zarrextra'; -export { Ok, Err, isOk, isErr, unwrap, unwrapOr } from 'zarrextra'; +export type { LazyZarrArray, Result, ZAttrsAny, ZarrTree } from 'zarrextra'; +export { ATTRS_KEY, Err, isErr, isOk, Ok, unwrap, unwrapOr, ZARRAY_KEY } from 'zarrextra'; /** * Used internally when passing around properties of a spatialdata object to be used by the models/loaders. diff --git a/packages/core/src/workers/index.ts b/packages/core/src/workers/index.ts index d0cc4f14..07a5a2fb 100644 --- a/packages/core/src/workers/index.ts +++ b/packages/core/src/workers/index.ts @@ -20,9 +20,9 @@ export { } from './pointsWorkerClient.js'; export type { + ParquetRowGroupBytesChunk, + ParquetWorkerPayload, PointsWorkerMessage, PointsWorkerRequest, PointsWorkerResponse, - ParquetRowGroupBytesChunk, - ParquetWorkerPayload, } from './pointsWorkerProtocol.js'; diff --git a/packages/core/src/workers/points-worker.ts b/packages/core/src/workers/points-worker.ts index a54f2807..5594eec9 100644 --- a/packages/core/src/workers/points-worker.ts +++ b/packages/core/src/workers/points-worker.ts @@ -1,12 +1,12 @@ import { tableFromIPC, tableToIPC } from 'apache-arrow'; -import { - buildFeatureCatalogFromColumns, -} from '../pointsFeatures.js'; -import { - filterColumnarByFeatureCodes, -} from '../pointsTiling.js'; import { getParquetModule, type ParquetModule } from '../parquetWasmLoader.js'; -import type { PointsWorkerMessage, PointsWorkerRequest, PointsWorkerResponse } from './pointsWorkerProtocol.js'; +import { buildFeatureCatalogFromColumns } from '../pointsFeatures.js'; +import { filterColumnarByFeatureCodes } from '../pointsTiling.js'; +import type { + PointsWorkerMessage, + PointsWorkerRequest, + PointsWorkerResponse, +} from './pointsWorkerProtocol.js'; import { countFeatureCodesFromArray, decodeGeometryWithFeaturesFromPayload, @@ -35,7 +35,9 @@ function toInt32Array(values: ArrayLike): Int32Array { return Int32Array.from(values); } -function handleFilterColumnar(request: Extract) { +function handleFilterColumnar( + request: Extract +) { const filtered = filterColumnarByFeatureCodes( { shape: request.zs ? [3, request.xs.length] : [2, request.xs.length], @@ -216,12 +218,11 @@ async function scanTablesForFeatureCounts( if (request.rowGroups?.length && parquetModule.readParquetRowGroup) { for (const chunk of request.rowGroups) { const table = tableFromIPC( - parquetModule.readParquetRowGroup( - chunk.schemaBytes, - chunk.rowGroupBytes, - chunk.rowGroupIndex, - { columns } - ).intoIPCStream() + parquetModule + .readParquetRowGroup(chunk.schemaBytes, chunk.rowGroupBytes, chunk.rowGroupIndex, { + columns, + }) + .intoIPCStream() ); scanTableFeatureCounts(table, request.featureKey, request.featureCodeColumnName, counts); } @@ -263,7 +264,7 @@ async function scanPayloadByFeatureCodes( scannedRows: number; } ): Promise<{ matchedRows: number; scannedRows: number }> { - const hasZ = request.axisNames.includes('z'); + const _hasZ = request.axisNames.includes('z'); const columns = [ ...request.axisNames, request.featureKey, @@ -279,12 +280,11 @@ async function scanPayloadByFeatureCodes( break; } const table = tableFromIPC( - parquetModule.readParquetRowGroup( - chunk.schemaBytes, - chunk.rowGroupBytes, - chunk.rowGroupIndex, - { columns } - ).intoIPCStream() + parquetModule + .readParquetRowGroup(chunk.schemaBytes, chunk.rowGroupBytes, chunk.rowGroupIndex, { + columns, + }) + .intoIPCStream() ); input.scannedRows += table.numRows; input.matchedRows = scanTableByFeatureCodes({ @@ -386,12 +386,11 @@ async function handleScanMortonRowGroupsInBounds( const zs: number[] = []; for (const chunk of request.rowGroups) { const table = tableFromIPC( - parquetModule.readParquetRowGroup( - chunk.schemaBytes, - chunk.rowGroupBytes, - chunk.rowGroupIndex, - { columns } - ).intoIPCStream() + parquetModule + .readParquetRowGroup(chunk.schemaBytes, chunk.rowGroupBytes, chunk.rowGroupIndex, { + columns, + }) + .intoIPCStream() ); scanMortonTableInBounds({ table, @@ -524,5 +523,3 @@ self.onmessage = (event: MessageEvent) => { self.postMessage(reply); }); }; - -export {}; diff --git a/packages/core/src/workers/pointsWorkerClient.ts b/packages/core/src/workers/pointsWorkerClient.ts index 800b1c70..e694303a 100644 --- a/packages/core/src/workers/pointsWorkerClient.ts +++ b/packages/core/src/workers/pointsWorkerClient.ts @@ -1,6 +1,6 @@ import { tableFromIPC } from 'apache-arrow'; -import type { PointsColumnarData } from '../spatialViewFit.js'; import type { PointsFeatureCatalog } from '../pointsTiling.js'; +import type { PointsColumnarData } from '../spatialViewFit.js'; import { columnarDataFromWorkerResult, type ParquetRowGroupBytesChunk, @@ -84,7 +84,10 @@ function ensureWorkerListener() { }; } -function postRequest(request: PointsWorkerRequest, transferables: Transferable[] = []): Promise { +function postRequest( + request: PointsWorkerRequest, + transferables: Transferable[] = [] +): Promise { const activeWorker = worker; if (!activeWorker) { return Promise.reject(new Error('Points worker is not enabled')); @@ -164,10 +167,9 @@ export function enablePointsWorker(options: { workerUrl?: string | URL } = {}) { } else { // Inline URL so Vite dev apps can bundle the worker; @vite-ignore keeps lib build // emitting a runtime relative URL to dist/points-worker.js (not /assets/...). - worker = new Worker( - new URL(/* @vite-ignore */ './points-worker.js', import.meta.url), - { type: 'module' } - ); + worker = new Worker(new URL(/* @vite-ignore */ './points-worker.js', import.meta.url), { + type: 'module', + }); } ensureWorkerListener(); enabled = true; @@ -205,12 +207,14 @@ export async function filterColumnarByFeatureCodesInWorker( return filterColumnarByFeatureCodes(data, featureCodes, sourceFeatureCodes); } - const xs = data.data[0] instanceof Float32Array - ? data.data[0] - : Float32Array.from(data.data[0] as ArrayLike); - const ys = data.data[1] instanceof Float32Array - ? data.data[1] - : Float32Array.from(data.data[1] as ArrayLike); + const xs = + data.data[0] instanceof Float32Array + ? data.data[0] + : Float32Array.from(data.data[0] as ArrayLike); + const ys = + data.data[1] instanceof Float32Array + ? data.data[1] + : Float32Array.from(data.data[1] as ArrayLike); const zs = data.data[2] ? data.data[2] instanceof Float32Array ? data.data[2] diff --git a/packages/core/src/workers/pointsWorkerProtocol.ts b/packages/core/src/workers/pointsWorkerProtocol.ts index db244a20..0dde7825 100644 --- a/packages/core/src/workers/pointsWorkerProtocol.ts +++ b/packages/core/src/workers/pointsWorkerProtocol.ts @@ -1,5 +1,5 @@ -import type { PointsColumnarData } from '../spatialViewFit.js'; import type { PointsFeatureCatalog } from '../pointsTiling.js'; +import type { PointsColumnarData } from '../spatialViewFit.js'; export type ParquetRowGroupBytesChunk = { schemaBytes: Uint8Array; diff --git a/packages/core/src/workers/pointsWorkerScan.ts b/packages/core/src/workers/pointsWorkerScan.ts index 9b2edde4..01652212 100644 --- a/packages/core/src/workers/pointsWorkerScan.ts +++ b/packages/core/src/workers/pointsWorkerScan.ts @@ -1,4 +1,4 @@ -import { tableFromIPC, type Table } from 'apache-arrow'; +import { type Table, tableFromIPC } from 'apache-arrow'; import { accumulateFeatureCatalogFromTable, buildFeatureCatalogFromColumns, @@ -122,12 +122,7 @@ export async function decodeParquetPayloadToTable( if (!readParquetRowGroup) { throw new Error('readParquetRowGroup is unavailable'); } - return decodeParquetRowGroupsToTable( - readParquetRowGroup, - payload.rowGroups, - columns, - maxRows - ); + return decodeParquetRowGroupsToTable(readParquetRowGroup, payload.rowGroups, columns, maxRows); } if (payload.parts?.length) { return decodeParquetPartsToTable(readParquet, payload.parts, columns, maxRows); diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index e876296c..ba84c050 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "ES2020", "module": "ESNext", - "lib": ["ES2020", "WebWorker", "DOM"], + "lib": ["ES2022", "WebWorker", "DOM"], "moduleResolution": "bundler", "paths": { "zarrextra": ["../zarrextra/dist/index.d.ts"] diff --git a/packages/layers/package.json b/packages/layers/package.json index f0622c50..94e72e75 100644 --- a/packages/layers/package.json +++ b/packages/layers/package.json @@ -8,7 +8,7 @@ "exports": { ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.js" + "default": "./dist/index.js" } }, "files": [ diff --git a/packages/layers/src/LabelsBitmaskTileLayer.ts b/packages/layers/src/LabelsBitmaskTileLayer.ts index 11bde7ad..6f1e0fbd 100644 --- a/packages/layers/src/LabelsBitmaskTileLayer.ts +++ b/packages/layers/src/LabelsBitmaskTileLayer.ts @@ -1,7 +1,7 @@ import type { GetPickingInfoParams, PickingInfo } from '@deck.gl/core'; -import { project32, picking } from '@deck.gl/core'; -import { Matrix4 } from '@math.gl/core'; +import { picking, project32 } from '@deck.gl/core'; import { XRLayer } from '@hms-dbmi/viv'; +import { Matrix4 } from '@math.gl/core'; import { fs, labelsBitmaskUniforms, vs } from './labelsBitmaskLayerShaders'; function getNormalizedColor(color?: readonly number[]): [number, number, number] { @@ -60,18 +60,6 @@ function getLabelAtPixel( return { labelId: labelValue, selection: selections?.[0] }; } -type LabelsBitmaskTileLayerProps = { - channelColors?: Array<[number, number, number]>; - channelsFilled?: boolean[]; - channelOpacities?: number[]; - channelOutlineOpacities?: number[]; - channelsVisible?: boolean[]; - channelStrokeWidths?: number[]; - maxZoom?: number; - opacity?: number; - zoom?: number; -}; - const UntypedXRLayer = XRLayer as any; export class LabelsBitmaskTileLayer extends UntypedXRLayer { @@ -86,6 +74,7 @@ export class LabelsBitmaskTileLayer extends UntypedXRLayer { channelStrokeWidths: { type: 'array', value: [1.5], compare: true }, }; + // biome-ignore lint/complexity/noUselessConstructor: widens the base UntypedXRLayer constructor so `new LabelsBitmaskTileLayer(props)` typechecks. constructor(...args: any[]) { super(...args); } diff --git a/packages/layers/src/PointsLayer.ts b/packages/layers/src/PointsLayer.ts index 1b58b658..c60b4859 100644 --- a/packages/layers/src/PointsLayer.ts +++ b/packages/layers/src/PointsLayer.ts @@ -1,19 +1,21 @@ -import type { Matrix4 } from '@math.gl/core'; import type { UpdateParameters } from '@deck.gl/core'; -import { filterColumnarByFeatureCodesInWorker } from '@spatialdata/core'; -import { CompositeLayer } from 'deck.gl'; +import type { Matrix4 } from '@math.gl/core'; +import { applyRenderCapToColumnar, filterColumnarByFeatureCodesInWorker } from '@spatialdata/core'; import type { Layer, LayersList } from 'deck.gl'; -import type { PointsRenderResource } from './pointsLoader.js'; -import type { TileDebugStore } from './pointsTiledDebugHooks.js'; -import type { ColumnarNdarrayPointsBatch } from './pointsLoader.js'; -import { filterBatchSignature, featureFilterAwaitingRowCodes, hasPreloadedRowFeatureCodes } from './pointsFeatureCodes.js'; +import { CompositeLayer } from 'deck.gl'; +import { + featureFilterAwaitingRowCodes, + filterBatchSignature, + hasPreloadedRowFeatureCodes, +} from './pointsFeatureCodes.js'; +import type { ColumnarNdarrayPointsBatch, PointsRenderResource } from './pointsLoader.js'; import { resolvePointsRenderStrategy } from './pointsRenderStrategies.js'; -import { applyRenderCapToColumnar } from '@spatialdata/core'; import { DEFAULT_POINT_RADIUS_MAX_PIXELS, DEFAULT_POINT_RADIUS_MIN_PIXELS, DEFAULT_POINT_SIZE, } from './pointsScatterLayer.js'; +import type { TileDebugStore } from './pointsTiledDebugHooks.js'; export interface PointsLayerProps { id: string; @@ -147,9 +149,7 @@ export class PointsLayer extends CompositeLayer { if ( preloadedBatch && canFilter && - (rowCodesBecameReady || - signature !== state.filteredBatchSignature || - !state.filteredBatch) + (rowCodesBecameReady || signature !== state.filteredBatchSignature || !state.filteredBatch) ) { void this.ensureFilteredBatch(preloadedBatch, signature); } @@ -218,5 +218,5 @@ export class PointsLayer extends CompositeLayer { } } -export { filterPreloadedBatch }; export { featureCodesSignature, filterBatchSignature } from './pointsFeatureCodes.js'; +export { filterPreloadedBatch }; diff --git a/packages/layers/src/SpatialLayer.ts b/packages/layers/src/SpatialLayer.ts index 64b57afe..7f40175b 100644 --- a/packages/layers/src/SpatialLayer.ts +++ b/packages/layers/src/SpatialLayer.ts @@ -1,4 +1,4 @@ -import { CompositeLayer, type LayersList, type Layer } from 'deck.gl'; +import { CompositeLayer, type Layer, type LayersList } from 'deck.gl'; import { createShapesDeckLayer, type ShapesLayerPickEvent, diff --git a/packages/layers/src/engine/PointsDataEngine.ts b/packages/layers/src/engine/PointsDataEngine.ts index cd586930..160c384d 100644 --- a/packages/layers/src/engine/PointsDataEngine.ts +++ b/packages/layers/src/engine/PointsDataEngine.ts @@ -1,14 +1,17 @@ import { DEFAULT_POINTS_MEMORY_CAP, featureCodeMapFromCatalog, - remapRowFeatureCodes, type PointsElement, type PointsFeatureCatalog, type PointsLoadProgress, type PointsLoadResult, + remapRowFeatureCodes, } from '@spatialdata/core'; -import { pointsRenderResourceSignature, resolvePointsRenderResource } from '../resolvePointsRenderResource.js'; import type { PointsRenderResource } from '../pointsLoader.js'; +import { + pointsRenderResourceSignature, + resolvePointsRenderResource, +} from '../resolvePointsRenderResource.js'; /** * Framework-agnostic points loading/caching/resolution engine. @@ -335,9 +338,7 @@ export class PointsDataEngine { // was made in. Pass that map; the core call ignores it for indexed // elements (which match on their code column instead). const featureCodeByName = - entry.featureCodeColumn === true - ? undefined - : featureCodeMapFromCatalog(entry.catalog); + entry.featureCodeColumn === true ? undefined : featureCodeMapFromCatalog(entry.catalog); //todo streamy version const result = await element.loadPointsMatchingFeatureCodes({ featureCodes, @@ -451,10 +452,10 @@ export class PointsDataEngine { * if no selection has ever settled. Deliberately NOT keyed to the current * selection: while a new selection's scan is in flight, this keeps returning the * previous selection's batch so the render shows those points instead of - * blanking for the (potentially multi-second) scan. - * - * The resource is cached on the matched batch and only changes identity when the - * batch does, so panning doesn't reset the composite. + * blanking for the (potentially multi-second) scan. + * + * The resource is cached on the matched batch and only changes identity when the + * batch does, so panning doesn't reset the composite. * Pair with `getMatchingLoadState` (exact-signature) * for the "is the current selection loaded" question. */ @@ -609,7 +610,10 @@ export class PointsDataEngine { const existing = this.entries.get(key); // (1) Existing data covers this cap without a reload (it is complete, or a // truncated batch the lowered cap doesn't outgrow). - if (existing?.data !== undefined && PointsDataEngine.batchAdequateForCap(existing.data, memoryCap)) { + if ( + existing?.data !== undefined && + PointsDataEngine.batchAdequateForCap(existing.data, memoryCap) + ) { // Cancel a now-unneeded in-flight load (e.g. the cap was raised then // lowered back to what we already hold). if (existing.loading) { diff --git a/packages/layers/src/geoArrowStrategies.ts b/packages/layers/src/geoArrowStrategies.ts index a2b68f97..a2ace9fa 100644 --- a/packages/layers/src/geoArrowStrategies.ts +++ b/packages/layers/src/geoArrowStrategies.ts @@ -1,5 +1,4 @@ import type { Layer, LayersList } from 'deck.gl'; -import type { PointsLayer } from './PointsLayer.js'; import type { PointsRenderStrategy } from './pointsRenderStrategies.js'; export const geoArrowBinaryStrategy: PointsRenderStrategy = { diff --git a/packages/layers/src/index.ts b/packages/layers/src/index.ts index ae59f2cd..dd5a6971 100644 --- a/packages/layers/src/index.ts +++ b/packages/layers/src/index.ts @@ -1,93 +1,42 @@ -export { SpatialLayer } from './SpatialLayer'; -export type { SpatialLayerProps, SpatialShapesSublayer } from './spatialLayerProps'; -export type { SpatialLayerRuntimeProps } from './SpatialLayer'; -export { LabelsLayer, MAX_LABEL_CHANNELS } from './LabelsLayer'; -export type { LabelsLayerProps, LabelsSelection } from './LabelsLayer'; -export { - createShapesDeckLayer, - buildShapesPrebuiltData, - DEFAULT_SHAPE_STROKE_WIDTH, - DEFAULT_SHAPE_STROKE_WIDTH_MAX_PIXELS, - DEFAULT_SHAPE_STROKE_WIDTH_MIN_PIXELS, - DEFAULT_SHAPE_STROKE_WIDTH_UNITS, - buildShapeFeatureStateRuntime, - EMPTY_SHAPE_FEATURE_STATE_RUNTIME, - isShapeFeatureStateRuntime, - normalizeShapeFeatureState, - resolveShapeFeatureFromPick, - resolveShapeFeatureFromPickInfo, - resolveShapeTooltipFromPickInfo, - resolveShapeTooltipRowIndex, - type ShapesLayerPickEvent, - type ShapeCircleRenderDatum, - type ShapeFeatureRenderDatum, - type ShapeFeatureStateInput, - type ShapeFeatureStateRuntime, - type ShapePolygonRenderDatum, - type SpatialShapesRuntimeSublayer, - type ShapesPrebuiltData, - type ShapesRenderDataLike, - type ShapeStrokeWidthUnits, - type ShapeTooltipRuntimeData, - type GeoarrowTableLike, -} from './shapesLayer'; -export { - buildShapeFillColorByFeatureId, - DEFAULT_SHAPE_CATEGORICAL_PALETTE, - DEFAULT_SHAPE_NUMERIC_RAMP, - resolveShapeFillColorMode, -} from './shapeColorEncoding'; -export type { - BuildShapeFillColorByFeatureIdOptions, - ShapeFillColorMode, - ShapeRgbColor, - ShapeRgbaColor, -} from './shapeColorEncoding'; -export { - spatialLayerPropsSchema, - spatialSublayerSchema, - migrateSpatialLayerProps, - SPATIAL_LAYER_PROPS_SCHEMA_VERSION, -} from './spatialLayerProps'; +// Framework-agnostic points loading/caching engine (LayerDataEngine step 1b). export { - getRenderStackEntryIds, - getRenderStackHostLayerIds, - renderStackEntrySchema, - renderStackGroupEntrySchema, - renderStackHostEntrySchema, - renderStackSchema, - renderStackSpatialElementTypeSchema, - renderStackSpatialEntrySchema, - RENDER_STACK_SCHEMA_VERSION, -} from './renderStack'; -export type { - RenderStack, - RenderStackEntry, - RenderStackGroupEntry, - RenderStackHostEntry, - RenderStackSpatialElementType, - RenderStackSpatialEntry, -} from './renderStack'; -export { PointsLayer } from './PointsLayer'; + PointsDataEngine, + type PointsDataEngineCallbacks, + type PointsLoadStatus, + type PointsLoadTarget, + type PointsMatchingLoadState, +} from './engine/PointsDataEngine.js'; +export type { LabelsLayerProps, LabelsSelection } from './LabelsLayer'; +export { LabelsLayer, MAX_LABEL_CHANNELS } from './LabelsLayer'; export type { PointsLayerProps } from './PointsLayer'; +export { PointsLayer } from './PointsLayer'; +export { featureCodeToCssColor, featureCodeToRgb } from './pointsFeatureColor.js'; +export { PointsFeatureColorExtension } from './pointsFeatureColorExtension.js'; export { - columnarBatchFromPointData, - pointDataFromColumnarBatch, type ArrowRecordBatchPointsBatch, type ColumnarNdarrayPointsBatch, + columnarBatchFromPointData, type PointData, type PointsBatch, type PointsBatchFormat, type PointsEncodingKind, - type PointsLoadInBoundsOptions, type PointsLoader, type PointsLoaderCapabilities, + type PointsLoadInBoundsOptions, type PointsRenderResource, + pointDataFromColumnarBatch, } from './pointsLoader.js'; export { - createPointsRenderResource, coreLoaderToPointsLoader, + createPointsRenderResource, } from './pointsLoaderAdapter.js'; +// Points render-resource resolution and load planning. +// Relocated from @spatialdata/vis (SpatialCanvas) per +// docs/plans/layer-data-engine-decomposition.md — these are framework-agnostic +// (no React) and belong in layers. Re-exported from vis for MDV compatibility. +export * from './pointsLoadPlan.js'; +export type { PointsRenderAttributes } from './pointsRenderAttributes.js'; +export { buildPointsAttributes } from './pointsRenderAttributes.js'; export { DEFAULT_POINT_RADIUS_MAX_PIXELS, DEFAULT_POINT_RADIUS_MIN_PIXELS, @@ -96,43 +45,92 @@ export { POINT_SIZE_ZOOM_REFERENCE, zoomScaledPointSize, } from './pointsScatterLayer.js'; -export { PointsFeatureColorExtension } from './pointsFeatureColorExtension.js'; -export { featureCodeToRgb, featureCodeToCssColor } from './pointsFeatureColor.js'; -export { buildPointsAttributes } from './pointsRenderAttributes.js'; -export type { PointsRenderAttributes } from './pointsRenderAttributes.js'; -export type { PointsTileHandle, PointsTileLoadResult } from './pointsTileLoadCallbacks.js'; -export { - createTileDebugStore, - createTiledPointsDebugHooks, - type TileDebugStore, - type TiledPointsDebugState, -} from './pointsTiledDebugHooks.js'; export { - POINTS_TILE_DEBUG_PICK_KIND, formatPointsTileDebugTooltip, isPointsTileDebugPickObject, - reduceTileDebugEntries, - tileDebugEntriesSignature, - tileDebugStatusFillColor, - tileDebugStatusLineColor, + POINTS_TILE_DEBUG_PICK_KIND, type PointsTileDebugEntry, type PointsTileDebugPickObject, type PointsTileLoadProgress, type PointsTileStatus, + reduceTileDebugEntries, + tileDebugEntriesSignature, + tileDebugStatusFillColor, + tileDebugStatusLineColor, } from './pointsTileDebug.js'; - -// Points render-resource resolution and load planning. -// Relocated from @spatialdata/vis (SpatialCanvas) per -// docs/plans/layer-data-engine-decomposition.md — these are framework-agnostic -// (no React) and belong in layers. Re-exported from vis for MDV compatibility. -export * from './pointsLoadPlan.js'; +export { + createTileDebugStore, + createTiledPointsDebugHooks, + type TileDebugStore, + type TiledPointsDebugState, +} from './pointsTiledDebugHooks.js'; +export type { PointsTileHandle, PointsTileLoadResult } from './pointsTileLoadCallbacks.js'; +export type { + RenderStack, + RenderStackEntry, + RenderStackGroupEntry, + RenderStackHostEntry, + RenderStackSpatialElementType, + RenderStackSpatialEntry, +} from './renderStack'; +export { + getRenderStackEntryIds, + getRenderStackHostLayerIds, + RENDER_STACK_SCHEMA_VERSION, + renderStackEntrySchema, + renderStackGroupEntrySchema, + renderStackHostEntrySchema, + renderStackSchema, + renderStackSpatialElementTypeSchema, + renderStackSpatialEntrySchema, +} from './renderStack'; export * from './resolvePointsRenderResource.js'; - -// Framework-agnostic points loading/caching engine (LayerDataEngine step 1b). +export type { SpatialLayerRuntimeProps } from './SpatialLayer'; +export { SpatialLayer } from './SpatialLayer'; +export type { + BuildShapeFillColorByFeatureIdOptions, + ShapeFillColorMode, + ShapeRgbaColor, + ShapeRgbColor, +} from './shapeColorEncoding'; export { - PointsDataEngine, - type PointsDataEngineCallbacks, - type PointsLoadStatus, - type PointsLoadTarget, - type PointsMatchingLoadState, -} from './engine/PointsDataEngine.js'; + buildShapeFillColorByFeatureId, + DEFAULT_SHAPE_CATEGORICAL_PALETTE, + DEFAULT_SHAPE_NUMERIC_RAMP, + resolveShapeFillColorMode, +} from './shapeColorEncoding'; +export { + buildShapeFeatureStateRuntime, + buildShapesPrebuiltData, + createShapesDeckLayer, + DEFAULT_SHAPE_STROKE_WIDTH, + DEFAULT_SHAPE_STROKE_WIDTH_MAX_PIXELS, + DEFAULT_SHAPE_STROKE_WIDTH_MIN_PIXELS, + DEFAULT_SHAPE_STROKE_WIDTH_UNITS, + EMPTY_SHAPE_FEATURE_STATE_RUNTIME, + type GeoarrowTableLike, + isShapeFeatureStateRuntime, + normalizeShapeFeatureState, + resolveShapeFeatureFromPick, + resolveShapeFeatureFromPickInfo, + resolveShapeTooltipFromPickInfo, + resolveShapeTooltipRowIndex, + type ShapeCircleRenderDatum, + type ShapeFeatureRenderDatum, + type ShapeFeatureStateInput, + type ShapeFeatureStateRuntime, + type ShapePolygonRenderDatum, + type ShapeStrokeWidthUnits, + type ShapesLayerPickEvent, + type ShapesPrebuiltData, + type ShapesRenderDataLike, + type ShapeTooltipRuntimeData, + type SpatialShapesRuntimeSublayer, +} from './shapesLayer'; +export type { SpatialLayerProps, SpatialShapesSublayer } from './spatialLayerProps'; +export { + migrateSpatialLayerProps, + SPATIAL_LAYER_PROPS_SCHEMA_VERSION, + spatialLayerPropsSchema, + spatialSublayerSchema, +} from './spatialLayerProps'; diff --git a/packages/layers/src/mortonTiledStrategy.ts b/packages/layers/src/mortonTiledStrategy.ts index 1c7dc687..053a404e 100644 --- a/packages/layers/src/mortonTiledStrategy.ts +++ b/packages/layers/src/mortonTiledStrategy.ts @@ -1,6 +1,6 @@ import { COORDINATE_SYSTEM } from '@deck.gl/core'; -import { PolygonLayer, TileLayer } from 'deck.gl'; import type { Layer, LayersList } from 'deck.gl'; +import { PolygonLayer, TileLayer } from 'deck.gl'; import type { PointsLayer } from './PointsLayer.js'; import { boundsFromTileBbox, @@ -9,22 +9,17 @@ import { scatterBoundsFromTileBbox, tileHandleFromDeckTile, } from './pointsBbox.js'; +import { featureCodesSignature } from './pointsFeatureCodes.js'; import type { ColumnarNdarrayPointsBatch } from './pointsLoader.js'; -import { - DEFAULT_POINT_RADIUS_MAX_PIXELS, - DEFAULT_POINT_RADIUS_MIN_PIXELS, - DEFAULT_POINT_SIZE, - renderColumnarScatterLayer, -} from './pointsScatterLayer.js'; import type { PointsRenderStrategy } from './pointsRenderStrategies.js'; -import { featureCodesSignature } from './pointsFeatureCodes.js'; -import { createTiledPointsDebugHooks } from './pointsTiledDebugHooks.js'; +import { DEFAULT_POINT_SIZE, renderColumnarScatterLayer } from './pointsScatterLayer.js'; import { POINTS_TILE_DEBUG_PICK_KIND, pointsTileDebugPolygonData, tileDebugStatusFillColor, tileDebugStatusLineColor, } from './pointsTileDebug.js'; +import { createTiledPointsDebugHooks } from './pointsTiledDebugHooks.js'; function isAbortError(error: unknown) { return error instanceof DOMException && error.name === 'AbortError'; diff --git a/packages/layers/src/pointsBbox.ts b/packages/layers/src/pointsBbox.ts index 3cad54ff..6d5bf2d6 100644 --- a/packages/layers/src/pointsBbox.ts +++ b/packages/layers/src/pointsBbox.ts @@ -21,10 +21,7 @@ export function isPointTileBbox(value: unknown): value is PointTileBbox { ); } -export function intersectBounds( - query: SpatialBounds, - clip: SpatialBounds -): SpatialBounds | null { +export function intersectBounds(query: SpatialBounds, clip: SpatialBounds): SpatialBounds | null { const minX = Math.max(query.minX, clip.minX); const maxX = Math.min(query.maxX, clip.maxX); const minY = Math.max(query.minY, clip.minY); @@ -44,9 +41,7 @@ export function boundsFromTileBbox(bbox: PointTileBbox): SpatialBounds { }; } -export function scatterBoundsFromTileBbox( - bbox: PointTileBbox -): [number, number, number, number] { +export function scatterBoundsFromTileBbox(bbox: PointTileBbox): [number, number, number, number] { return [bbox.left, bbox.top, bbox.right, bbox.bottom]; } diff --git a/packages/layers/src/pointsFeatureCodes.ts b/packages/layers/src/pointsFeatureCodes.ts index 32acefb5..2672a57a 100644 --- a/packages/layers/src/pointsFeatureCodes.ts +++ b/packages/layers/src/pointsFeatureCodes.ts @@ -22,7 +22,10 @@ export function featureCodesSignature(featureCodes: readonly number[] | undefine if (featureCodes.length === 0) { return 'none'; } - return featureCodes.slice().sort((left, right) => left - right).join(','); + return featureCodes + .slice() + .sort((left, right) => left - right) + .join(','); } export function preloadedFeatureCodesSignature( diff --git a/packages/layers/src/pointsFeatureColor.ts b/packages/layers/src/pointsFeatureColor.ts index dab2603c..69f1f115 100644 --- a/packages/layers/src/pointsFeatureColor.ts +++ b/packages/layers/src/pointsFeatureColor.ts @@ -20,14 +20,14 @@ export const PFC_GOLDEN_RATIO_CONJUGATE = 0.6180339887498949; export const PFC_LIGHTNESS = 0.72; export const PFC_CHROMA = 0.32; -const TWO_PI = 6.28318530717958648; +const TWO_PI = 6.283185307179586; function fract(x: number): number { return x - Math.floor(x); } function linearToSrgb(x: number): number { - return x <= 0.0031308 ? x * 12.92 : 1.055 * Math.pow(Math.max(x, 0), 1 / 2.4) - 0.055; + return x <= 0.0031308 ? x * 12.92 : 1.055 * Math.max(x, 0) ** (1 / 2.4) - 0.055; } function channel255(x: number): number { @@ -45,11 +45,7 @@ function oklabToRgb255(L: number, a: number, b: number): [number, number, number const r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s; const g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s; const bl = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s; - return [ - channel255(linearToSrgb(r)), - channel255(linearToSrgb(g)), - channel255(linearToSrgb(bl)), - ]; + return [channel255(linearToSrgb(r)), channel255(linearToSrgb(g)), channel255(linearToSrgb(bl))]; } /** Categorical colour for a feature code as `[r, g, b]` in 0–255. */ diff --git a/packages/layers/src/pointsFeatureColorExtension.ts b/packages/layers/src/pointsFeatureColorExtension.ts index 693d8f32..99f8eeb7 100644 --- a/packages/layers/src/pointsFeatureColorExtension.ts +++ b/packages/layers/src/pointsFeatureColorExtension.ts @@ -1,5 +1,5 @@ -import { LayerExtension } from '@deck.gl/core'; import type { Layer } from '@deck.gl/core'; +import { LayerExtension } from '@deck.gl/core'; import { PFC_CHROMA, PFC_GOLDEN_RATIO_CONJUGATE, PFC_LIGHTNESS } from './pointsFeatureColor.js'; /** Render a JS number as a GLSL float literal (always with a decimal point, so an diff --git a/packages/layers/src/pointsLoadPlan.ts b/packages/layers/src/pointsLoadPlan.ts index b20e8a57..a767b223 100644 --- a/packages/layers/src/pointsLoadPlan.ts +++ b/packages/layers/src/pointsLoadPlan.ts @@ -1,4 +1,4 @@ -import { resolvePointsMemoryCap, type PointsTilingMetadata } from '@spatialdata/core'; +import { type PointsTilingMetadata, resolvePointsMemoryCap } from '@spatialdata/core'; export interface PointsPreloadCacheKeyInput { pointsMemoryCap?: number; diff --git a/packages/layers/src/pointsLoader.ts b/packages/layers/src/pointsLoader.ts index 85c9eb92..ed0c0aaf 100644 --- a/packages/layers/src/pointsLoader.ts +++ b/packages/layers/src/pointsLoader.ts @@ -1,4 +1,4 @@ -import type { SpatialBounds, PointsElement, PointsLoadMode } from '@spatialdata/core'; +import type { PointsElement, PointsLoadMode, SpatialBounds } from '@spatialdata/core'; export type PointsEncodingKind = | 'preloaded-columnar' diff --git a/packages/layers/src/pointsLoaderAdapter.ts b/packages/layers/src/pointsLoaderAdapter.ts index d4b9a238..40cce3fd 100644 --- a/packages/layers/src/pointsLoaderAdapter.ts +++ b/packages/layers/src/pointsLoaderAdapter.ts @@ -20,9 +20,9 @@ export type { PointsBatch, PointsBatchFormat, PointsEncodingKind, - PointsLoadInBoundsOptions, PointsLoader, PointsLoaderCapabilities, + PointsLoadInBoundsOptions, PointsRenderResource, } from './pointsLoader.js'; diff --git a/packages/layers/src/pointsRenderStrategies.ts b/packages/layers/src/pointsRenderStrategies.ts index 0dba7a61..30ce1aef 100644 --- a/packages/layers/src/pointsRenderStrategies.ts +++ b/packages/layers/src/pointsRenderStrategies.ts @@ -1,8 +1,12 @@ import type { Layer, LayersList } from 'deck.gl'; -import type { PointsEncodingKind, PointsLoader } from './pointsLoader.js'; -import type { PointsLayer } from './PointsLayer.js'; -import { geoArrowBinaryStrategy, geoArrowTiledStrategy, unsupportedPointsStrategy } from './geoArrowStrategies.js'; +import { + geoArrowBinaryStrategy, + geoArrowTiledStrategy, + unsupportedPointsStrategy, +} from './geoArrowStrategies.js'; import { mortonTiledStrategy } from './mortonTiledStrategy.js'; +import type { PointsLayer } from './PointsLayer.js'; +import type { PointsEncodingKind, PointsLoader } from './pointsLoader.js'; import { preloadedScatterStrategy } from './preloadedScatterStrategy.js'; export interface PointsRenderStrategy { diff --git a/packages/layers/src/pointsScatterLayer.ts b/packages/layers/src/pointsScatterLayer.ts index fcaa3320..c1373870 100644 --- a/packages/layers/src/pointsScatterLayer.ts +++ b/packages/layers/src/pointsScatterLayer.ts @@ -1,9 +1,8 @@ import type { Matrix4 } from '@math.gl/core'; -import { COORDINATE_SYSTEM } from '@deck.gl/core'; import { ScatterplotLayer } from 'deck.gl'; +import { PointsFeatureColorExtension } from './pointsFeatureColorExtension.js'; import type { ColumnarNdarrayPointsBatch } from './pointsLoader.js'; import { buildPointsAttributes } from './pointsRenderAttributes.js'; -import { PointsFeatureColorExtension } from './pointsFeatureColorExtension.js'; /** Orthographic zoom at which configured pointSize applies at full scale. */ export const POINT_SIZE_ZOOM_REFERENCE = 0; @@ -84,7 +83,7 @@ export function renderColumnarScatterLayer( return new ScatterplotLayer({ id, - coordinateSystem: "cartesian", + coordinateSystem: 'cartesian', data: { length: attributes.length, attributes: { diff --git a/packages/layers/src/pointsTileDebug.ts b/packages/layers/src/pointsTileDebug.ts index 62a45bb5..6fcc877d 100644 --- a/packages/layers/src/pointsTileDebug.ts +++ b/packages/layers/src/pointsTileDebug.ts @@ -1,13 +1,7 @@ import type { SpatialBounds } from '@spatialdata/core'; import type { PointsTileHandle, PointsTileLoadResult } from './pointsTileLoadCallbacks.js'; -export type PointsTileStatus = - | 'pending' - | 'loading' - | 'loaded' - | 'empty' - | 'error' - | 'aborted'; +export type PointsTileStatus = 'pending' | 'loading' | 'loaded' | 'empty' | 'error' | 'aborted'; export interface PointsTileLoadProgress { inFlight: number; @@ -37,9 +31,7 @@ export interface PointsTileDebugPickObject { entry: PointsTileDebugEntry; } -export function isPointsTileDebugPickObject( - value: unknown -): value is PointsTileDebugPickObject { +export function isPointsTileDebugPickObject(value: unknown): value is PointsTileDebugPickObject { if (!value || typeof value !== 'object') { return false; } @@ -176,7 +168,10 @@ export function reduceTileDebugEntries( loadMode: existing?.loadMode ?? completed?.loadMode, errorMessage: existing?.errorMessage ?? completed?.errorMessage, }; - if (completed && (status === 'loaded' || status === 'empty' || status === 'error' || status === 'aborted')) { + if ( + completed && + (status === 'loaded' || status === 'empty' || status === 'error' || status === 'aborted') + ) { entry = applyCompletedSnapshot(entry, completed); } next.set(tile.tileId, entry); diff --git a/packages/layers/src/pointsTileLoadCallbacks.ts b/packages/layers/src/pointsTileLoadCallbacks.ts index 43b93fd0..4739756e 100644 --- a/packages/layers/src/pointsTileLoadCallbacks.ts +++ b/packages/layers/src/pointsTileLoadCallbacks.ts @@ -1,4 +1,4 @@ -import type { SpatialBounds, PointsLoadMode } from '@spatialdata/core'; +import type { PointsLoadMode, SpatialBounds } from '@spatialdata/core'; import type { PointTileBbox } from './pointsBbox.js'; export interface PointsTileHandle { diff --git a/packages/layers/src/pointsTiledDebugHooks.ts b/packages/layers/src/pointsTiledDebugHooks.ts index 8daf978f..921273b5 100644 --- a/packages/layers/src/pointsTiledDebugHooks.ts +++ b/packages/layers/src/pointsTiledDebugHooks.ts @@ -1,11 +1,11 @@ -import type { PointsTileHandle, PointsTileLoadResult } from './pointsTileLoadCallbacks.js'; import { completedSnapshotFromLoadResult, - reduceTileDebugEntries, - tileDebugEntriesSignature, type PointsTileCompletedSnapshot, type PointsTileDebugEntry, + reduceTileDebugEntries, + tileDebugEntriesSignature, } from './pointsTileDebug.js'; +import type { PointsTileHandle, PointsTileLoadResult } from './pointsTileLoadCallbacks.js'; export interface TiledPointsDebugState { tileDebugEntries: PointsTileDebugEntry[]; diff --git a/packages/layers/src/preloadedScatterStrategy.ts b/packages/layers/src/preloadedScatterStrategy.ts index 2b473545..9486f5dd 100644 --- a/packages/layers/src/preloadedScatterStrategy.ts +++ b/packages/layers/src/preloadedScatterStrategy.ts @@ -1,13 +1,10 @@ import { applyRenderCapToColumnar } from '@spatialdata/core'; import type { Layer, LayersList } from 'deck.gl'; import type { PointsLayer } from './PointsLayer.js'; -import type { PointsRenderStrategy } from './pointsRenderStrategies.js'; import { featureFilterAwaitingRowCodes, filterBatchSignature } from './pointsFeatureCodes.js'; -import { - DEFAULT_POINT_SIZE, - renderColumnarScatterLayer, -} from './pointsScatterLayer.js'; import type { ColumnarNdarrayPointsBatch } from './pointsLoader.js'; +import type { PointsRenderStrategy } from './pointsRenderStrategies.js'; +import { DEFAULT_POINT_SIZE, renderColumnarScatterLayer } from './pointsScatterLayer.js'; function resolveScatterBatch(layer: PointsLayer): ColumnarNdarrayPointsBatch | undefined { const { featureCodes, preloadedFeatureCodes, renderCap } = layer.props; @@ -64,7 +61,6 @@ function resolveScatterBatch(layer: PointsLayer): ColumnarNdarrayPointsBatch | u export const preloadedScatterStrategy: PointsRenderStrategy = { renderLayers(layer): Layer | null | LayersList { const { - resource, opacity = 1, visible = true, pointSize = DEFAULT_POINT_SIZE, diff --git a/packages/layers/src/renderStack.ts b/packages/layers/src/renderStack.ts index 0006bdd4..867772d2 100644 --- a/packages/layers/src/renderStack.ts +++ b/packages/layers/src/renderStack.ts @@ -3,12 +3,7 @@ import { z } from 'zod'; /** Current serialized render-stack version; bump for breaking saved-stack changes. */ export const RENDER_STACK_SCHEMA_VERSION = 1 as const; -export const renderStackSpatialElementTypeSchema = z.enum([ - 'image', - 'shapes', - 'points', - 'labels', -]); +export const renderStackSpatialElementTypeSchema = z.enum(['image', 'shapes', 'points', 'labels']); const jsonishRecordSchema = z.record(z.string(), z.unknown()).default({}); diff --git a/packages/layers/src/resolvePointsRenderResource.ts b/packages/layers/src/resolvePointsRenderResource.ts index e0c46760..0ca52f38 100644 --- a/packages/layers/src/resolvePointsRenderResource.ts +++ b/packages/layers/src/resolvePointsRenderResource.ts @@ -3,11 +3,15 @@ import { type PointsElement, type PointsTilingMetadata, } from '@spatialdata/core'; -import { createPointsRenderResource } from './pointsLoaderAdapter.js'; import type { PointsRenderResource } from './pointsLoader.js'; +import { createPointsRenderResource } from './pointsLoaderAdapter.js'; export interface ResolvePointsRenderResourceCache { - preloaded?: { shape: number[]; data: ArrayLike[]; featureCodes?: ArrayLike } | null; + preloaded?: { + shape: number[]; + data: ArrayLike[]; + featureCodes?: ArrayLike; + } | null; tilingMetadata?: PointsTilingMetadata | null; metadataKnown?: boolean; } @@ -49,7 +53,7 @@ export function pointsRenderResourceSignature( const rowCount = cache.preloaded && cache.preloaded.shape.length >= 2 ? cache.preloaded.shape[1] - : cache.preloaded?.data[0]?.length ?? 0; + : (cache.preloaded?.data[0]?.length ?? 0); return [ element.key, options.preloadCacheKey ?? '', diff --git a/packages/layers/src/shapesLayer.ts b/packages/layers/src/shapesLayer.ts index dfb4791d..9a7893ed 100644 --- a/packages/layers/src/shapesLayer.ts +++ b/packages/layers/src/shapesLayer.ts @@ -136,7 +136,7 @@ function recordToRgbaMap( } const map = new Map(); for (const key in record) { - if (Object.prototype.hasOwnProperty.call(record, key)) { + if (Object.hasOwn(record, key)) { map.set(key, record[key]); } } diff --git a/packages/layers/src/spatialLayerProps.ts b/packages/layers/src/spatialLayerProps.ts index d7a64ea9..95a77158 100644 --- a/packages/layers/src/spatialLayerProps.ts +++ b/packages/layers/src/spatialLayerProps.ts @@ -88,13 +88,12 @@ export const spatialLayerPropsSchema = z.object({ export type SpatialLayerProps = z.infer; /** Version 0: pre-schema ad-hoc objects (empty or partial). */ -const spatialLayerPropsV0Schema = z - .looseObject({ - schemaVersion: z.never().optional(), - sublayers: z.array(z.unknown()).optional(), - viewMode: z.enum(['2d', '3d']).optional(), - globalTimeIndex: z.number().optional(), - }); +const spatialLayerPropsV0Schema = z.looseObject({ + schemaVersion: z.never().optional(), + sublayers: z.array(z.unknown()).optional(), + viewMode: z.enum(['2d', '3d']).optional(), + globalTimeIndex: z.number().optional(), +}); function migrateV0ToV1(raw: z.infer): SpatialLayerProps { const sublayersIn = raw.sublayers ?? []; diff --git a/packages/layers/tsconfig.json b/packages/layers/tsconfig.json index 19bc34f0..4d3f9c62 100644 --- a/packages/layers/tsconfig.json +++ b/packages/layers/tsconfig.json @@ -12,7 +12,7 @@ "skipLibCheck": true, "esModuleInterop": true, "resolveJsonModule": true, - "lib": ["ES2020", "DOM"] + "lib": ["ES2022", "DOM"] // Cross-package types resolve via node_modules to each package's built dist/*.d.ts // (topological build order guarantees deps are built first). Matches core's pattern. }, diff --git a/packages/react/package.json b/packages/react/package.json index 5ac74264..aae65099 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -8,7 +8,7 @@ "exports": { ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.js" + "default": "./dist/index.js" } }, "files": [ diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index fc8795e9..df4aff37 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,2 +1,2 @@ -export * from './provider/SpatialDataProvider'; export * from './hooks/useSpatialData'; +export * from './provider/SpatialDataProvider'; diff --git a/packages/react/src/provider/SpatialDataProvider.tsx b/packages/react/src/provider/SpatialDataProvider.tsx index 43a6f60a..828c3e2f 100644 --- a/packages/react/src/provider/SpatialDataProvider.tsx +++ b/packages/react/src/provider/SpatialDataProvider.tsx @@ -1,10 +1,10 @@ -import { type PropsWithChildren, createContext, useContext, useMemo } from 'react'; import { - readZarr, type ElementName, + readZarr, type SpatialData, type StoreReference, } from '@spatialdata/core'; +import { createContext, type PropsWithChildren, useContext, useMemo } from 'react'; type SpatialDataContextValue = { spatialDataPromise: Promise | null; diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json index 6b4f3b1a..a3036049 100644 --- a/packages/react/tsconfig.json +++ b/packages/react/tsconfig.json @@ -13,7 +13,7 @@ "skipLibCheck": true, "esModuleInterop": true, "resolveJsonModule": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ES2022", "DOM", "DOM.Iterable"], // Cross-package types resolve via node_modules to each package's built dist/*.d.ts // (topological build order guarantees deps are built first). Matches core's pattern. "types": ["react", "react-dom"] diff --git a/packages/vis/package.json b/packages/vis/package.json index b8220d1b..e8cb0d0e 100644 --- a/packages/vis/package.json +++ b/packages/vis/package.json @@ -8,7 +8,7 @@ "exports": { ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.js" + "default": "./dist/index.js" } }, "files": [ diff --git a/packages/vis/src/ImageView/index.tsx b/packages/vis/src/ImageView/index.tsx index ddbfc202..c8691ff4 100644 --- a/packages/vis/src/ImageView/index.tsx +++ b/packages/vis/src/ImageView/index.tsx @@ -1,16 +1,16 @@ -import { VivViewer, getDefaultInitialViewState } from '@hms-dbmi/viv'; +import { getDefaultInitialViewState, VivViewer } from '@hms-dbmi/viv'; import { - DEFAULT_CHANNEL_STATE, - VivProvider, createVivStores, + DEFAULT_CHANNEL_STATE, resolveRasterSource, useChannelsStore, useChannelsStoreApi, + useImage, useLoader, useViewerStore, useViewerStoreApi, + VivProvider, } from '@spatialdata/avivatorish'; -import { useImage } from '@spatialdata/avivatorish'; import { useSpatialData } from '@spatialdata/react'; import { useMeasure } from '@uidotdev/usehooks'; import { DetailView, ScaleBarView } from '@vivjs/views'; diff --git a/packages/vis/src/Sketch/index.tsx b/packages/vis/src/Sketch/index.tsx index a4ec647e..8daedb69 100644 --- a/packages/vis/src/Sketch/index.tsx +++ b/packages/vis/src/Sketch/index.tsx @@ -1,13 +1,11 @@ -import { useEffect, useState, type CSSProperties } from 'react'; import { SpatialDataProvider, useSpatialData } from '@spatialdata/react'; -import SpatialDataTree from '../Tree'; -import Table from '../Table'; -import ImageView from '../ImageView'; -import Transforms from '../Transforms'; +import { type CSSProperties, useEffect, useState } from 'react'; import SpatialCanvas from '../SpatialCanvas'; +import Transforms from '../Transforms'; +import SpatialDataTree from '../Tree'; import { - DEFAULT_DEMO_SPATIALDATA_URL, buildDemoPageHref, + DEFAULT_DEMO_SPATIALDATA_URL, getSpatialDataUrlFromSearchParams, } from './demoUrl'; @@ -70,6 +68,7 @@ function DataSource({ children }: React.PropsWithChildren) { ); } +// biome-ignore lint/correctness/noUnusedVariables: dev-only debug view, toggled via the commented usage below. function Repr() { const { spatialData } = useSpatialData(); return
{spatialData?.toString()}
; diff --git a/packages/vis/src/SpatialCanvas/ImageChannelPanel.tsx b/packages/vis/src/SpatialCanvas/ImageChannelPanel.tsx index 4ee30a78..23331779 100644 --- a/packages/vis/src/SpatialCanvas/ImageChannelPanel.tsx +++ b/packages/vis/src/SpatialCanvas/ImageChannelPanel.tsx @@ -1,9 +1,9 @@ -import type { CSSProperties } from 'react'; import { clampVivSelectionsToAxes, MAX_CHANNELS, mergeLayerChannelState, } from '@spatialdata/avivatorish'; +import type { CSSProperties } from 'react'; import type { ChannelConfig } from './types'; import type { ImageLoaderData } from './useLayerData'; diff --git a/packages/vis/src/SpatialCanvas/ImageLayerContext.tsx b/packages/vis/src/SpatialCanvas/ImageLayerContext.tsx index b7cd0ab3..cece6d20 100644 --- a/packages/vis/src/SpatialCanvas/ImageLayerContext.tsx +++ b/packages/vis/src/SpatialCanvas/ImageLayerContext.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, type PropsWithChildren } from 'react'; +import { createContext, type PropsWithChildren, useContext } from 'react'; import type { ImageLoaderData, LayerLoadState } from './useLayerData'; export type ImageLayerContextRegistry = { diff --git a/packages/vis/src/SpatialCanvas/LayerOrderList.tsx b/packages/vis/src/SpatialCanvas/LayerOrderList.tsx index 08fbd476..411d8e05 100644 --- a/packages/vis/src/SpatialCanvas/LayerOrderList.tsx +++ b/packages/vis/src/SpatialCanvas/LayerOrderList.tsx @@ -72,12 +72,14 @@ export function LayerOrderList({ const layer = layers[id]; if (!layer) return null; return ( + // biome-ignore lint/a11y/noStaticElementInteractions: drag-reorder drop target; keyboard a11y not yet implemented for this dev panel.
onDrop(e, id)} > + {/* biome-ignore lint/a11y/noStaticElementInteractions: drag handle; keyboard a11y not yet implemented for this dev panel. */} onDragStart(e, id)} diff --git a/packages/vis/src/SpatialCanvas/PointsFeatureFilterPanel.tsx b/packages/vis/src/SpatialCanvas/PointsFeatureFilterPanel.tsx index 6afb8034..f2890251 100644 --- a/packages/vis/src/SpatialCanvas/PointsFeatureFilterPanel.tsx +++ b/packages/vis/src/SpatialCanvas/PointsFeatureFilterPanel.tsx @@ -1,9 +1,9 @@ import { featureCodeToCssColor } from '@spatialdata/layers'; import type { CSSProperties } from 'react'; import { useEffect, useMemo, useState } from 'react'; -import { usePointsFeatureState } from './PointsFeatureState'; import { useSpatialCanvasActions } from './context'; import { describeFeatureRowState, featureRowOpacity } from './featureRowState'; +import { usePointsFeatureState } from './PointsFeatureState'; import type { PointsLayerConfig } from './types'; // we need a pass on how we manage styles diff --git a/packages/vis/src/SpatialCanvas/PointsFeatureState.tsx b/packages/vis/src/SpatialCanvas/PointsFeatureState.tsx index 90ab286d..c343b978 100644 --- a/packages/vis/src/SpatialCanvas/PointsFeatureState.tsx +++ b/packages/vis/src/SpatialCanvas/PointsFeatureState.tsx @@ -24,8 +24,8 @@ */ import type { PointsDataEngine, PointsLoadTarget } from '@spatialdata/layers'; import { - type ReactNode, createContext, + type ReactNode, useCallback, useContext, useMemo, diff --git a/packages/vis/src/SpatialCanvas/PointsLayerPanel.tsx b/packages/vis/src/SpatialCanvas/PointsLayerPanel.tsx index e8a1d549..fbc54fe9 100644 --- a/packages/vis/src/SpatialCanvas/PointsLayerPanel.tsx +++ b/packages/vis/src/SpatialCanvas/PointsLayerPanel.tsx @@ -1,8 +1,8 @@ import { DEFAULT_POINTS_MEMORY_CAP } from '@spatialdata/core'; import type { PointsDataEngine, PointsLoadTarget } from '@spatialdata/layers'; +import { useSpatialCanvasActions } from './context'; import { PointsFeatureFilterPanel } from './PointsFeatureFilterPanel'; import { PointsFeatureStateProvider, usePointsFeatureState } from './PointsFeatureState'; -import { useSpatialCanvasActions } from './context'; import type { PointsLayerConfig } from './types'; export interface PointsLayerPanelProps { diff --git a/packages/vis/src/SpatialCanvas/ShapeFillColorPanel.tsx b/packages/vis/src/SpatialCanvas/ShapeFillColorPanel.tsx index 93012b70..ae2de612 100644 --- a/packages/vis/src/SpatialCanvas/ShapeFillColorPanel.tsx +++ b/packages/vis/src/SpatialCanvas/ShapeFillColorPanel.tsx @@ -1,4 +1,4 @@ -import { useId, type CSSProperties } from 'react'; +import { type CSSProperties, useId } from 'react'; import type { ShapesLayerConfig } from './types'; const helperTextStyle: CSSProperties = { diff --git a/packages/vis/src/SpatialCanvas/SpatialCanvasViewer.tsx b/packages/vis/src/SpatialCanvas/SpatialCanvasViewer.tsx index 20d17dc6..44aa797e 100644 --- a/packages/vis/src/SpatialCanvas/SpatialCanvasViewer.tsx +++ b/packages/vis/src/SpatialCanvas/SpatialCanvasViewer.tsx @@ -13,29 +13,28 @@ import { } from 'react'; import { createPortal } from 'react-dom'; import { ensureCodecWorkers } from '../codecWorkers'; -import { ImageLayerContextProvider } from './ImageLayerContext'; -import { - type SpatialCanvasTooltipRenderProps, - SpatialFeatureTooltip, - type SpatialFeatureTooltipData, -} from './SpatialFeatureTooltip'; -import { SpatialViewer } from './SpatialViewer'; -import { VivLoaderRegistryProvider } from './VivLoaderRegistry'; import { - type HoverPointerEvent, getDeckFromDeckGlRef, + type HoverPointerEvent, isHoverDuringDrag, resolveHoverFeatureTooltip, } from './featureTooltipHover'; +import { ImageLayerContextProvider } from './ImageLayerContext'; import { type RenderStackHostLayerResolver, type RenderStackLayerInputs, - type UnknownRenderStackHostLayerHandler, renderStackOrder, renderStackToLayerInputs, resolveRenderStackHostLayers, sortLayersByRenderStackOrder, + type UnknownRenderStackHostLayerHandler, } from './renderStackAdapters'; +import { + type SpatialCanvasTooltipRenderProps, + SpatialFeatureTooltip, + type SpatialFeatureTooltipData, +} from './SpatialFeatureTooltip'; +import { SpatialViewer } from './SpatialViewer'; import type { ElementsByType, LayerConfig, ShapesLayerPickEvent, ViewState } from './types'; import { type LabelFeaturePickEventData, @@ -44,6 +43,7 @@ import { } from './useLayerData'; import { useViewInteractionGate } from './useViewInteractionGate'; import { getAvailableElements } from './utils'; +import { VivLoaderRegistryProvider } from './VivLoaderRegistry'; import type { VivImageExtensionResolver, VivImagePassthroughOptions, @@ -259,6 +259,7 @@ export function useSpatialCanvasRendererFromLayerInputs({ ]); return sortDeckLayers ? sortLayersByRenderStackOrder(composed, resolvedLayerOrder) : composed; }, [externalDeckLayers, generatedDeckLayers, hostDeckLayers, resolvedLayerOrder, sortDeckLayers]); + // biome-ignore lint/correctness/useExhaustiveDependencies: intentionally depends on stable members of layerData (getter + memoized flags), not the freshly-created object; see note and eslint-disable below. const vivLayerProps = useMemo( () => layerData.getVivLayerProps(), // useLayerData returns a fresh object every render, so we intentionally depend diff --git a/packages/vis/src/SpatialCanvas/SpatialFeatureTooltip.tsx b/packages/vis/src/SpatialCanvas/SpatialFeatureTooltip.tsx index 98709db1..9d482284 100644 --- a/packages/vis/src/SpatialCanvas/SpatialFeatureTooltip.tsx +++ b/packages/vis/src/SpatialCanvas/SpatialFeatureTooltip.tsx @@ -1,10 +1,10 @@ -import type { CSSProperties } from 'react'; import type { SpatialFeatureTooltipData, SpatialFeatureTooltipItem, SpatialFeatureTooltipSection, } from '@spatialdata/core'; import { formatSpatialElementLabel } from '@spatialdata/core'; +import type { CSSProperties } from 'react'; export type { SpatialFeatureTooltipData, diff --git a/packages/vis/src/SpatialCanvas/SpatialViewer.tsx b/packages/vis/src/SpatialCanvas/SpatialViewer.tsx index b74ef299..fdca0741 100644 --- a/packages/vis/src/SpatialCanvas/SpatialViewer.tsx +++ b/packages/vis/src/SpatialCanvas/SpatialViewer.tsx @@ -10,14 +10,13 @@ * - Otherwise: uses simplified functional component with DetailView */ -import type { Deck } from '@deck.gl/core'; import { DetailView } from '@hms-dbmi/viv'; -import { DeckGL } from 'deck.gl'; import type { DeckGLProps, DeckGLRef, Layer, PickingInfo } from 'deck.gl'; +import { DeckGL } from 'deck.gl'; import { type RefObject, useCallback, useId, useMemo } from 'react'; -import VivSpatialViewer, { normalizeVivLayers } from './VivSpatialViewer'; import type { ViewState } from './types'; import type { ImageLayerConfig } from './useLayerData'; +import VivSpatialViewer, { normalizeVivLayers } from './VivSpatialViewer'; export interface SpatialViewerProps { /** Viewport width */ diff --git a/packages/vis/src/SpatialCanvas/VivLoaderRegistry.tsx b/packages/vis/src/SpatialCanvas/VivLoaderRegistry.tsx index 5e63c51d..21a3bbda 100644 --- a/packages/vis/src/SpatialCanvas/VivLoaderRegistry.tsx +++ b/packages/vis/src/SpatialCanvas/VivLoaderRegistry.tsx @@ -1,8 +1,8 @@ import { - type OmeZarrMultiscalesSource, loadOmeZarrMultiscalesData, + type OmeZarrMultiscalesSource, } from '@spatialdata/avivatorish'; -import { type PropsWithChildren, createContext, useContext, useMemo } from 'react'; +import { createContext, type PropsWithChildren, useContext, useMemo } from 'react'; export type VivLoaderRegistryValue = { /** Multiscales pixel sources for an OME-Zarr store or URL (SpatialCanvas image path). */ diff --git a/packages/vis/src/SpatialCanvas/VivSpatialViewer.tsx b/packages/vis/src/SpatialCanvas/VivSpatialViewer.tsx index a0589113..0bf712b1 100644 --- a/packages/vis/src/SpatialCanvas/VivSpatialViewer.tsx +++ b/packages/vis/src/SpatialCanvas/VivSpatialViewer.tsx @@ -12,9 +12,8 @@ * Structured to allow gradual refactoring to hooks in the future. */ -import { ScaleBarLayer, getDefaultInitialViewState } from '@hms-dbmi/viv'; +import { getDefaultInitialViewState, ScaleBarLayer } from '@hms-dbmi/viv'; import { DetailView, ScaleBarView } from '@vivjs/views'; -import { DeckGL } from 'deck.gl'; import type { DeckGLProps, DeckGLRef, @@ -24,6 +23,7 @@ import type { OrthographicViewState, PickingInfo, } from 'deck.gl'; +import { DeckGL } from 'deck.gl'; import equal from 'fast-deep-equal'; import * as React from 'react'; import type { ViewState } from './types'; @@ -363,7 +363,10 @@ class VivSpatialViewer extends React.PureComponent, and consume the usePoints* hooks. +export { PointsFeatureStateProvider, usePointsFeatureState } from './PointsFeatureState'; export type { - HoverTooltipMode, - VivImageExtensionResolver, - VivImageLayerContext, - VivImagePropsResolver, -} from './SpatialCanvasViewer'; -export { useImageLayerContext, ImageLayerContextProvider } from './ImageLayerContext'; -export type { ImageLayerContextValue } from './ImageLayerContext'; -export { mergeVivImagePassthroughProps } from './vivImagePassthrough'; -export type { VivImagePassthroughOptions } from './vivImagePassthrough'; -export { - useLayerChannelState, - mergeLayerChannelState, - type LayerChannelConfig, - type LayerChannelDefaults, -} from '@spatialdata/avivatorish'; + RenderStackHostLayerResolver, + RenderStackLayerInputs, + UnknownRenderStackHostLayerHandler, +} from './renderStackAdapters'; export { renderStackOrder, renderStackToLayerInputs, @@ -51,26 +33,42 @@ export { sortLayersByRenderStackOrder, } from './renderStackAdapters'; export type { - RenderStackHostLayerResolver, - RenderStackLayerInputs, - UnknownRenderStackHostLayerHandler, -} from './renderStackAdapters'; -export type { + HoverTooltipMode, LabelsSpatialFeaturePickEvent, ShapesSpatialFeaturePickEvent, - SpatialFeaturePickEvent, SpatialCanvasViewerProps, SpatialCanvasViewerRenderTooltip, + SpatialFeaturePickEvent, + VivImageExtensionResolver, + VivImageLayerContext, + VivImagePropsResolver, } from './SpatialCanvasViewer'; -export type { SpatialCanvasProps } from './index'; +export { + composeSpatialDeckLayers, + SpatialCanvasViewer, + shouldAutoFitSpatialView, + shouldRenderInternalTooltip, + useSpatialCanvasRenderer, + useSpatialCanvasRendererFromLayerInputs, +} from './SpatialCanvasViewer'; +export { + type SpatialCanvasTooltipRenderProps, + SpatialFeatureTooltip, + type SpatialFeatureTooltipData, + type SpatialFeatureTooltipItem, + type SpatialFeatureTooltipProps, + type SpatialFeatureTooltipSection, +} from './SpatialFeatureTooltip'; +export type { SpatialViewerProps } from './SpatialViewer'; +export { SpatialViewer } from './SpatialViewer'; +export type { SpatialCanvasStoreApi } from './stores'; +export { createSpatialCanvasStore } from './stores'; +export type * from './types'; export type { ImageLayerConfig as VivImageLayerConfig, ImageLoaderData, LayerLoadState, } from './useLayerData'; -// Reactive points feature state. Headless (panel-less) consumers read -// `pointsEngine` + `resolvePointsTarget` off the renderer-hook result, wrap a -// subtree in , and consume the usePoints* hooks. -export { PointsFeatureStateProvider, usePointsFeatureState } from './PointsFeatureState'; -export type { PointsFeatureState, PointsFeatureStateProviderProps } from './PointsFeatureState'; -export type { PointsDataEngine, PointsLoadTarget } from '@spatialdata/layers'; +export { VivSpatialViewer } from './VivSpatialViewer'; +export type { VivImagePassthroughOptions } from './vivImagePassthrough'; +export { mergeVivImagePassthroughProps } from './vivImagePassthrough'; diff --git a/packages/vis/src/SpatialCanvas/renderStackAdapters.ts b/packages/vis/src/SpatialCanvas/renderStackAdapters.ts index 31b7419b..d52824b5 100644 --- a/packages/vis/src/SpatialCanvas/renderStackAdapters.ts +++ b/packages/vis/src/SpatialCanvas/renderStackAdapters.ts @@ -76,7 +76,10 @@ export function resolveRenderStackHostLayers( return layers; } -export function renderStackOrder(renderStack: RenderStack | undefined, fallback: string[]): string[] { +export function renderStackOrder( + renderStack: RenderStack | undefined, + fallback: string[] +): string[] { if (!renderStack) return fallback; return renderStack.entries.flatMap((entry) => { if (entry.kind === 'spatial' || entry.kind === 'host') { diff --git a/packages/vis/src/SpatialCanvas/renderers/imageRenderer.ts b/packages/vis/src/SpatialCanvas/renderers/imageRenderer.ts index 66237210..c57a367a 100644 --- a/packages/vis/src/SpatialCanvas/renderers/imageRenderer.ts +++ b/packages/vis/src/SpatialCanvas/renderers/imageRenderer.ts @@ -7,8 +7,8 @@ import type { Matrix4 } from '@math.gl/core'; import { - type OmeZarrMultiscalesSource, loadOmeZarrMultiscalesData, + type OmeZarrMultiscalesSource, } from '@spatialdata/avivatorish'; import type { ImageElement, LabelsElement } from '@spatialdata/core'; import type { Layer } from 'deck.gl'; @@ -42,7 +42,7 @@ export interface ImageLayerRenderConfig { * This function is kept for API consistency but returns null - layers are * created by calling view.getLayers() in the viewer component. */ -export function renderImageLayer(config: ImageLayerRenderConfig): Layer | null { +export function renderImageLayer(_config: ImageLayerRenderConfig): Layer | null { // Image layers are handled via Viv's view.getLayers() system in the viewer // This function exists for API consistency but doesn't create layers directly return null; diff --git a/packages/vis/src/SpatialCanvas/renderers/index.ts b/packages/vis/src/SpatialCanvas/renderers/index.ts index 8c25d239..7e0733ae 100644 --- a/packages/vis/src/SpatialCanvas/renderers/index.ts +++ b/packages/vis/src/SpatialCanvas/renderers/index.ts @@ -5,6 +5,6 @@ * ready to be composed into the final view. */ -export { renderImageLayer, type ImageLayerRenderConfig } from './imageRenderer'; +export { type ImageLayerRenderConfig, renderImageLayer } from './imageRenderer'; +export { type LabelsLayerRenderConfig, renderLabelsLayer } from './labelsRenderer'; export { renderShapesLayer, type ShapesLayerRenderConfig } from './shapesRenderer'; -export { renderLabelsLayer, type LabelsLayerRenderConfig } from './labelsRenderer'; diff --git a/packages/vis/src/SpatialCanvas/renderers/labelsRenderer.ts b/packages/vis/src/SpatialCanvas/renderers/labelsRenderer.ts index bed4fc65..1a1f3770 100644 --- a/packages/vis/src/SpatialCanvas/renderers/labelsRenderer.ts +++ b/packages/vis/src/SpatialCanvas/renderers/labelsRenderer.ts @@ -6,8 +6,8 @@ * channel instead of showing the raw integer values as grayscale. */ -import { LabelsLayer } from '@spatialdata/layers'; import type { Matrix4 } from '@math.gl/core'; +import { LabelsLayer } from '@spatialdata/layers'; import type { Layer } from 'deck.gl'; export interface LabelsLayerRenderConfig { diff --git a/packages/vis/src/SpatialCanvas/renderers/shapesRenderer.ts b/packages/vis/src/SpatialCanvas/renderers/shapesRenderer.ts index 82ab495c..3089bcf9 100644 --- a/packages/vis/src/SpatialCanvas/renderers/shapesRenderer.ts +++ b/packages/vis/src/SpatialCanvas/renderers/shapesRenderer.ts @@ -9,14 +9,14 @@ import type { Matrix4 } from '@math.gl/core'; import type { ShapesElement, ShapesRenderData, SpatialFeatureTooltipData } from '@spatialdata/core'; import { + createShapesDeckLayer, DEFAULT_SHAPE_STROKE_WIDTH, DEFAULT_SHAPE_STROKE_WIDTH_MAX_PIXELS, DEFAULT_SHAPE_STROKE_WIDTH_MIN_PIXELS, DEFAULT_SHAPE_STROKE_WIDTH_UNITS, - type ShapeStrokeWidthUnits, type ShapeFeatureStateRuntime, + type ShapeStrokeWidthUnits, type ShapesPrebuiltData, - createShapesDeckLayer, } from '@spatialdata/layers'; import type { Layer } from 'deck.gl'; diff --git a/packages/vis/src/SpatialCanvas/stores.ts b/packages/vis/src/SpatialCanvas/stores.ts index 31927c0c..d66bbaf9 100644 --- a/packages/vis/src/SpatialCanvas/stores.ts +++ b/packages/vis/src/SpatialCanvas/stores.ts @@ -6,13 +6,7 @@ */ import { createStore } from 'zustand/vanilla'; -import type { - SpatialCanvasState, - SpatialCanvasActions, - SpatialCanvasStore, - LayerConfig, - ViewState, -} from './types'; +import type { LayerConfig, SpatialCanvasState, SpatialCanvasStore } from './types'; const initialState: SpatialCanvasState = { coordinateSystem: null, diff --git a/packages/vis/src/SpatialCanvas/types.ts b/packages/vis/src/SpatialCanvas/types.ts index 275667d6..0ce4938a 100644 --- a/packages/vis/src/SpatialCanvas/types.ts +++ b/packages/vis/src/SpatialCanvas/types.ts @@ -3,7 +3,7 @@ */ import type { Matrix4 } from '@math.gl/core'; -import type { AnyElement, SpatialElement } from '@spatialdata/core'; +import type { SpatialElement } from '@spatialdata/core'; import type { ShapeFillColorMode, ShapeStrokeWidthUnits, diff --git a/packages/vis/src/SpatialCanvas/useLayerData.ts b/packages/vis/src/SpatialCanvas/useLayerData.ts index 6c9cdb19..52032f34 100644 --- a/packages/vis/src/SpatialCanvas/useLayerData.ts +++ b/packages/vis/src/SpatialCanvas/useLayerData.ts @@ -8,8 +8,8 @@ import { getImageSize } from '@hms-dbmi/viv'; import type { Matrix4 } from '@math.gl/core'; import { - COLOR_PALLETE, buildDefaultSelection, + COLOR_PALLETE, clampVivSelectionsToAxes, getMultiSelectionStats, getVivSelectionAxisSizes, @@ -19,15 +19,6 @@ import { } from '@spatialdata/avivatorish'; import { type AxisAlignedBounds, - type ImageElement, - type LabelsElement, - type LabelsTooltipMetadata, - type PointsElement, - type ShapesElement, - type ShapesRenderData, - type ShapesTooltipMetadata, - type SpatialData, - type SpatialFeatureTooltipData, attachTooltipElementContext, boundsFromCircles, boundsFromImagePixelExtents, @@ -35,36 +26,44 @@ import { boundsFromPolygons, getPhysicalSizeScalingMatrixFromMeta, getTooltipSignature, + type ImageElement, + type LabelsElement, + type LabelsTooltipMetadata, loadAssociatedTableFeatureRows, loadLabelsTooltipMetadata, loadShapesTooltipMetadata, + type PointsElement, resolvePointsMemoryCap, resolveTooltipItems, + type ShapesElement, + type ShapesRenderData, + type ShapesTooltipMetadata, + type SpatialData, + type SpatialFeatureTooltipData, unionBoundsList, } from '@spatialdata/core'; import { + buildShapeFeatureStateRuntime, + buildShapeFillColorByFeatureId, + buildShapesPrebuiltData, EMPTY_SHAPE_FEATURE_STATE_RUNTIME, PointsDataEngine, PointsLayer, type PointsLoadTarget, type PointsRenderResource, + resolveShapeFeatureFromPick, + resolveShapeTooltipFromPickInfo, + resolveShapeTooltipRowIndex, type ShapeFeatureRenderDatum, type ShapeFeatureStateRuntime, type ShapeFillColorMode, type ShapesPrebuiltData, - buildShapeFeatureStateRuntime, - buildShapeFillColorByFeatureId, - buildShapesPrebuiltData, - resolveShapeFeatureFromPick, - resolveShapeTooltipFromPickInfo, - resolveShapeTooltipRowIndex, } from '@spatialdata/layers'; import type { Layer } from 'deck.gl'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useVivLoaderRegistry } from './VivLoaderRegistry'; import { - type VivLoaderMetadata, applyPerChannelFallbackWithoutOmero, + type VivLoaderMetadata, } from './imageLoaderChannelDefaults'; import { createImageLoader } from './renderers/imageRenderer'; import { renderLabelsLayer } from './renderers/labelsRenderer'; @@ -76,9 +75,10 @@ import type { LayerConfig, ShapesLayerConfig, } from './types'; +import { useVivLoaderRegistry } from './VivLoaderRegistry'; import { - type VivImagePassthroughOptions, mergeVivImagePassthroughProps, + type VivImagePassthroughOptions, } from './vivImagePassthrough'; export interface ImageLoaderData { @@ -268,7 +268,7 @@ function getShapeFillColorAlpha(config: ShapesLayerConfig): number { } function getShapeFillColorSignature(config: LayerConfig | undefined): string { - if (!config || config.type !== 'shapes' || !config.fillColorByColumn?.columnName) { + if (config?.type !== 'shapes' || !config.fillColorByColumn?.columnName) { return ''; } const mode: ShapeFillColorMode = config.fillColorByColumn.mode; @@ -476,7 +476,7 @@ export function useLayerData( layers: Record, layerOrder: string[], availableElements: ElementsByType, - coordinateSystem: string | null, + _coordinateSystem: string | null, spatialData?: SpatialData, vivPassthrough?: VivImagePassthroughOptions ): UseLayerDataResult { @@ -1137,30 +1137,33 @@ export function useLayerData( pointsEngine, ]); - const reloadElement = useCallback((type: string, key: string) => { - const loaded = loadedDataRef.current; - if (type === 'shapes') { - loaded.shapes.delete(key); - loaded.worldBounds.delete(`shapes:${key}`); - // Clear prebuilt data for every layer that maps to this element key. - for (const [layerId, config] of Object.entries(layersRef.current)) { - if (config.type === 'shapes' && config.elementKey === key) { - loaded.shapePrebuiltData.delete(layerId); - loaded.shapeFillColorData.delete(layerId); + const reloadElement = useCallback( + (type: string, key: string) => { + const loaded = loadedDataRef.current; + if (type === 'shapes') { + loaded.shapes.delete(key); + loaded.worldBounds.delete(`shapes:${key}`); + // Clear prebuilt data for every layer that maps to this element key. + for (const [layerId, config] of Object.entries(layersRef.current)) { + if (config.type === 'shapes' && config.elementKey === key) { + loaded.shapePrebuiltData.delete(layerId); + loaded.shapeFillColorData.delete(layerId); + } } + } else if (type === 'points') { + pointsEngine.evict(key); + loaded.worldBounds.delete(`points:${key}`); + } else if (type === 'image') { + loaded.images.delete(key); + loaded.worldBounds.delete(`image:${key}`); + } else if (type === 'labels') { + loaded.labels.delete(key); + loaded.worldBounds.delete(`labels:${key}`); } - } else if (type === 'points') { - pointsEngine.evict(key); - loaded.worldBounds.delete(`points:${key}`); - } else if (type === 'image') { - loaded.images.delete(key); - loaded.worldBounds.delete(`image:${key}`); - } else if (type === 'labels') { - loaded.labels.delete(key); - loaded.worldBounds.delete(`labels:${key}`); - } - // The useEffect will pick up the missing data and reload - }, [pointsEngine]); + // The useEffect will pick up the missing data and reload + }, + [pointsEngine] + ); const getStableSelections = useCallback((key: string, selections: RasterSelection[]) => { const signature = serializeRasterSelections(selections); @@ -1173,23 +1176,26 @@ export function useLayerData( return value; }, []); - const hasRenderableLayerData = useCallback((layerId: string): boolean => { - const elem = resolveLayerElement(layerId, layersRef.current[layerId], elementMap.current); - if (!elem) return false; - if (elem.type === 'shapes') { - return loadedDataRef.current.shapes.has(elem.key); - } - if (elem.type === 'points') { - return pointsEngine.hasData(elem.key); - } - if (elem.type === 'image') { - return loadedDataRef.current.images.has(elem.key); - } - if (elem.type === 'labels') { - return loadedDataRef.current.labels.has(elem.key); - } - return false; - }, [pointsEngine]); + const hasRenderableLayerData = useCallback( + (layerId: string): boolean => { + const elem = resolveLayerElement(layerId, layersRef.current[layerId], elementMap.current); + if (!elem) return false; + if (elem.type === 'shapes') { + return loadedDataRef.current.shapes.has(elem.key); + } + if (elem.type === 'points') { + return pointsEngine.hasData(elem.key); + } + if (elem.type === 'image') { + return loadedDataRef.current.images.has(elem.key); + } + if (elem.type === 'labels') { + return loadedDataRef.current.labels.has(elem.key); + } + return false; + }, + [pointsEngine] + ); // --- Points feature state (filter panel) ----------------------------------- // The panel no longer reads point state through prop-drilled getters. Instead @@ -1197,14 +1203,11 @@ export function useLayerData( // + the `usePoints*` hooks), so its reactivity is self-contained and does not // depend on this hook's re-render or a `'use no memo'` escape hatch. All this // hook exposes is the engine and the element-key resolver the hooks need. - const resolvePointsTarget = useCallback( - (layerId: string): PointsLoadTarget | undefined => { - const elem = resolveLayerElement(layerId, layersRef.current[layerId], elementMap.current); - if (!elem || elem.type !== 'points') return undefined; - return { key: elem.key, layerId, element: elem.element as PointsElement }; - }, - [] - ); + const resolvePointsTarget = useCallback((layerId: string): PointsLoadTarget | undefined => { + const elem = resolveLayerElement(layerId, layersRef.current[layerId], elementMap.current); + if (elem?.type !== 'points') return undefined; + return { key: elem.key, layerId, element: elem.element as PointsElement }; + }, []); const getWorldBoundsForLayer = useCallback( (layerId: string): AxisAlignedBounds | null => { @@ -1301,223 +1304,226 @@ export function useLayerData( return unionBoundsList(list); }, [layerOrder, layers, getWorldBoundsForLayer]); - const getLayers = useCallback((options?: { pickingEnabled?: boolean }): Layer[] => { - // When the camera is moving we disable shape picking (autoHighlight + hover) - // to avoid deck re-rendering the full shape geometry into the picking buffer - // on every pointer move. Defaults to enabled. - const pickingEnabled = options?.pickingEnabled ?? true; - const deckLayers: Layer[] = []; - const loaded = loadedDataRef.current; + const getLayers = useCallback( + (options?: { pickingEnabled?: boolean }): Layer[] => { + // When the camera is moving we disable shape picking (autoHighlight + hover) + // to avoid deck re-rendering the full shape geometry into the picking buffer + // on every pointer move. Defaults to enabled. + const pickingEnabled = options?.pickingEnabled ?? true; + const deckLayers: Layer[] = []; + const loaded = loadedDataRef.current; - for (const layerId of layerOrder) { - const config = layers[layerId]; - if (!config?.visible) continue; + for (const layerId of layerOrder) { + const config = layers[layerId]; + if (!config?.visible) continue; - const elem = resolveLayerElement(layerId, config, elementMap.current); - if (!elem) continue; - - if (config.type === 'shapes') { - const shapeData = loaded.shapes.get(elem.key); - if (shapeData) { - const layer = renderShapesLayer({ - element: elem.element as ShapesElement, - id: layerId, - modelMatrix: elem.transform, - opacity: config.opacity, - visible: config.visible, - fillColor: config.fillColor, - strokeColor: config.strokeColor, - strokeWidth: config.strokeWidth, - strokeWidthUnits: config.strokeWidthUnits, - strokeWidthMinPixels: config.strokeWidthMinPixels, - strokeWidthMaxPixels: config.strokeWidthMaxPixels, - featureStateRuntime: getStableShapeFeatureStateRuntime( - layerId, - config, - loaded.shapeFillColorData.get(layerId), - stableShapeFeatureStateRef.current - ), - renderData: shapeData.renderData, - prebuilt: loaded.shapePrebuiltData.get(layerId)?.prebuilt, - pickingEnabled, - }); - if (layer) deckLayers.push(layer); - } - } else if (config.type === 'points') { - const element = elem.element as PointsElement; - const featureCodes = config.featureCodes; - const selectionActive = featureCodes !== undefined && featureCodes.length > 0; - - // Feature-index render scan: when a selection is active, load the WHOLE - // dataset's matching points (footer stats skip the row groups a selected - // feature can't live in), so features outside the resident preload window - // still render. The scan is idempotent per selection; kicking it here is a - // no-op once resident/in-flight. On settle it notifies → re-render → the - // matched resource appears below. `getMatchingResource` returns the LAST - // completed matched batch, so a selection change keeps showing the prior - // selection's points until the new scan settles (no blank mid-scan). - // - // Gated on scan capability: an authoritative code column (footer stats - // skip row groups) OR a dictionary-only element with a catalog loaded — - // there the scan reads the whole file and matches each row's feature_name - // against the catalog's code space, so a selected gene's points render - // even when they fall outside the resident preload window. Before any - // catalog loads (no shared code space) there is nothing to match names - // against, so it falls through to resident in-memory filtering. - const canFeatureScan = pointsEngine.supportsFeatureScan(elem.key); - let matchingResource: PointsRenderResource | null = null; - let partialResource: PointsRenderResource | null = null; - if (selectionActive && canFeatureScan) { - void pointsEngine.ensureMatchingFeaturesLoaded( - { key: elem.key, layerId, element }, - featureCodes, - resolvePointsMemoryCap(config.pointsMemoryCap) - ); - matchingResource = pointsEngine.getMatchingResource(element, elem.key); - // The in-flight scan's growing buffer (all matched chunks so far), drawn - // as an extra overlay sub-layer below so the base (resident preview / - // prior matched batch) stays visible while points progressively fill in. - partialResource = pointsEngine.getMatchingPartialResource(element, elem.key); - } + const elem = resolveLayerElement(layerId, config, elementMap.current); + if (!elem) continue; - if (matchingResource) { - // The matched batch covers the selection (or a superset of it, when the - // selection just shrank). Pass the batch's per-row codes + the current - // selection so the layer filters IN MEMORY down to the selected codes — - // this is what makes removing a feature a free filter instead of a - // re-scan. When the selection equals what was scanned, skip the filter - // (render the batch whole); the batch's own codes still drive colour. - const matchedRowCodes = pointsEngine.getMatchingRowFeatureCodes(elem.key); - const coveredSize = pointsEngine.getLoadedMatchingFeatureCodes(elem.key)?.size ?? 0; - const filterMatched = featureCodes !== undefined && featureCodes.length < coveredSize; - deckLayers.push( - new PointsLayer({ + if (config.type === 'shapes') { + const shapeData = loaded.shapes.get(elem.key); + if (shapeData) { + const layer = renderShapesLayer({ + element: elem.element as ShapesElement, id: layerId, - resource: matchingResource, modelMatrix: elem.transform, opacity: config.opacity, visible: config.visible, - pointSize: config.pointSize ?? 1, - ...(filterMatched ? { featureCodes } : {}), - ...(matchedRowCodes ? { preloadedFeatureCodes: matchedRowCodes } : {}), - ...(config.color ? { color: config.color } : {}), - ...(config.colorByFeature ? { colorByFeature: true } : {}), - }) - ); - } else { - // Resident batch: the default view (no selection), and an instant preview - // of the resident subset while the feature-index scan is still running. - // The engine returns a STABLE render resource (memoized by signature), so - // re-running getLayers every pan/zoom frame reuses the same loader - // identity and the composite does not reset its batch (no flashing). - const resource = pointsEngine.getResource(element, elem.key); - if (resource) { - const filterActive = featureCodes !== undefined; - // Row codes are needed to filter by feature AND to colour by feature. - // Colour-by-feature applies even with no filter ("all features"), so - // load/pass the codes whenever either is on — not just when filtering. - const needsRowCodes = filterActive || config.colorByFeature === true; - if (needsRowCodes && !pointsEngine.hasRowFeatureCodes(elem.key)) { - void pointsEngine.ensureRowFeatureCodes({ key: elem.key, layerId, element }); - } - const preloadedFeatureCodes = needsRowCodes - ? pointsEngine.getRowFeatureCodes(elem.key) - : undefined; + fillColor: config.fillColor, + strokeColor: config.strokeColor, + strokeWidth: config.strokeWidth, + strokeWidthUnits: config.strokeWidthUnits, + strokeWidthMinPixels: config.strokeWidthMinPixels, + strokeWidthMaxPixels: config.strokeWidthMaxPixels, + featureStateRuntime: getStableShapeFeatureStateRuntime( + layerId, + config, + loaded.shapeFillColorData.get(layerId), + stableShapeFeatureStateRef.current + ), + renderData: shapeData.renderData, + prebuilt: loaded.shapePrebuiltData.get(layerId)?.prebuilt, + pickingEnabled, + }); + if (layer) deckLayers.push(layer); + } + } else if (config.type === 'points') { + const element = elem.element as PointsElement; + const featureCodes = config.featureCodes; + const selectionActive = featureCodes !== undefined && featureCodes.length > 0; + + // Feature-index render scan: when a selection is active, load the WHOLE + // dataset's matching points (footer stats skip the row groups a selected + // feature can't live in), so features outside the resident preload window + // still render. The scan is idempotent per selection; kicking it here is a + // no-op once resident/in-flight. On settle it notifies → re-render → the + // matched resource appears below. `getMatchingResource` returns the LAST + // completed matched batch, so a selection change keeps showing the prior + // selection's points until the new scan settles (no blank mid-scan). + // + // Gated on scan capability: an authoritative code column (footer stats + // skip row groups) OR a dictionary-only element with a catalog loaded — + // there the scan reads the whole file and matches each row's feature_name + // against the catalog's code space, so a selected gene's points render + // even when they fall outside the resident preload window. Before any + // catalog loads (no shared code space) there is nothing to match names + // against, so it falls through to resident in-memory filtering. + const canFeatureScan = pointsEngine.supportsFeatureScan(elem.key); + let matchingResource: PointsRenderResource | null = null; + let partialResource: PointsRenderResource | null = null; + if (selectionActive && canFeatureScan) { + void pointsEngine.ensureMatchingFeaturesLoaded( + { key: elem.key, layerId, element }, + featureCodes, + resolvePointsMemoryCap(config.pointsMemoryCap) + ); + matchingResource = pointsEngine.getMatchingResource(element, elem.key); + // The in-flight scan's growing buffer (all matched chunks so far), drawn + // as an extra overlay sub-layer below so the base (resident preview / + // prior matched batch) stays visible while points progressively fill in. + partialResource = pointsEngine.getMatchingPartialResource(element, elem.key); + } + + if (matchingResource) { + // The matched batch covers the selection (or a superset of it, when the + // selection just shrank). Pass the batch's per-row codes + the current + // selection so the layer filters IN MEMORY down to the selected codes — + // this is what makes removing a feature a free filter instead of a + // re-scan. When the selection equals what was scanned, skip the filter + // (render the batch whole); the batch's own codes still drive colour. + const matchedRowCodes = pointsEngine.getMatchingRowFeatureCodes(elem.key); + const coveredSize = pointsEngine.getLoadedMatchingFeatureCodes(elem.key)?.size ?? 0; + const filterMatched = featureCodes !== undefined && featureCodes.length < coveredSize; deckLayers.push( new PointsLayer({ id: layerId, - resource, + resource: matchingResource, modelMatrix: elem.transform, opacity: config.opacity, visible: config.visible, - // Legacy renderPointsLayer defaulted radius to 1px; preserve that - // for parity (the composite's own default is smaller). pointSize: config.pointSize ?? 1, + ...(filterMatched ? { featureCodes } : {}), + ...(matchedRowCodes ? { preloadedFeatureCodes: matchedRowCodes } : {}), ...(config.color ? { color: config.color } : {}), ...(config.colorByFeature ? { colorByFeature: true } : {}), - ...(featureCodes ? { featureCodes } : {}), - ...(preloadedFeatureCodes ? { preloadedFeatureCodes } : {}), }) ); + } else { + // Resident batch: the default view (no selection), and an instant preview + // of the resident subset while the feature-index scan is still running. + // The engine returns a STABLE render resource (memoized by signature), so + // re-running getLayers every pan/zoom frame reuses the same loader + // identity and the composite does not reset its batch (no flashing). + const resource = pointsEngine.getResource(element, elem.key); + if (resource) { + const filterActive = featureCodes !== undefined; + // Row codes are needed to filter by feature AND to colour by feature. + // Colour-by-feature applies even with no filter ("all features"), so + // load/pass the codes whenever either is on — not just when filtering. + const needsRowCodes = filterActive || config.colorByFeature === true; + if (needsRowCodes && !pointsEngine.hasRowFeatureCodes(elem.key)) { + void pointsEngine.ensureRowFeatureCodes({ key: elem.key, layerId, element }); + } + const preloadedFeatureCodes = needsRowCodes + ? pointsEngine.getRowFeatureCodes(elem.key) + : undefined; + deckLayers.push( + new PointsLayer({ + id: layerId, + resource, + modelMatrix: elem.transform, + opacity: config.opacity, + visible: config.visible, + // Legacy renderPointsLayer defaulted radius to 1px; preserve that + // for parity (the composite's own default is smaller). + pointSize: config.pointSize ?? 1, + ...(config.color ? { color: config.color } : {}), + ...(config.colorByFeature ? { colorByFeature: true } : {}), + ...(featureCodes ? { featureCodes } : {}), + ...(preloadedFeatureCodes ? { preloadedFeatureCodes } : {}), + }) + ); + } } - } - // Overlay the in-flight scan's growing buffer as a SEPARATE sub-layer on - // top of whichever base layer was pushed above, so the base doesn't blank - // while points progressively fill in. Distinct id so deck keeps them as two - // layers. Filter it to the CURRENT selection with the partial's own per-row - // codes — mirroring the settled matched layer — so a feature deselected - // mid-scan stops rendering immediately instead of lingering until settle. - if (partialResource) { - const partialRowCodes = pointsEngine.getMatchingPartialRowFeatureCodes(elem.key); - deckLayers.push( - new PointsLayer({ - id: `${layerId}__partial`, - resource: partialResource, + // Overlay the in-flight scan's growing buffer as a SEPARATE sub-layer on + // top of whichever base layer was pushed above, so the base doesn't blank + // while points progressively fill in. Distinct id so deck keeps them as two + // layers. Filter it to the CURRENT selection with the partial's own per-row + // codes — mirroring the settled matched layer — so a feature deselected + // mid-scan stops rendering immediately instead of lingering until settle. + if (partialResource) { + const partialRowCodes = pointsEngine.getMatchingPartialRowFeatureCodes(elem.key); + deckLayers.push( + new PointsLayer({ + id: `${layerId}__partial`, + resource: partialResource, + modelMatrix: elem.transform, + opacity: config.opacity, + visible: config.visible, + pointSize: config.pointSize ?? 1, + ...(featureCodes ? { featureCodes } : {}), + ...(partialRowCodes ? { preloadedFeatureCodes: partialRowCodes } : {}), + ...(config.color ? { color: config.color } : {}), + ...(config.colorByFeature ? { colorByFeature: true } : {}), + }) + ); + } + } else if (config.type === 'labels') { + const labelsData = loaded.labels.get(elem.key); + if (labelsData) { + const ch = config.channels; + const rawSelections = + ch?.selections && ch.selections.length > 0 ? ch.selections : labelsData.selections; + const selections = + labelsData.selectionAxisSizes !== undefined + ? clampVivSelectionsToAxes(rawSelections, labelsData.selectionAxisSizes) + : rawSelections; + const stableSelections = getStableSelections(`labels:${layerId}`, selections); + + const layer = renderLabelsLayer({ + id: layerId, + loader: labelsData.loader, modelMatrix: elem.transform, opacity: config.opacity, visible: config.visible, - pointSize: config.pointSize ?? 1, - ...(featureCodes ? { featureCodes } : {}), - ...(partialRowCodes ? { preloadedFeatureCodes: partialRowCodes } : {}), - ...(config.color ? { color: config.color } : {}), - ...(config.colorByFeature ? { colorByFeature: true } : {}), - }) - ); - } - } else if (config.type === 'labels') { - const labelsData = loaded.labels.get(elem.key); - if (labelsData) { - const ch = config.channels; - const rawSelections = - ch?.selections && ch.selections.length > 0 ? ch.selections : labelsData.selections; - const selections = - labelsData.selectionAxisSizes !== undefined - ? clampVivSelectionsToAxes(rawSelections, labelsData.selectionAxisSizes) - : rawSelections; - const stableSelections = getStableSelections(`labels:${layerId}`, selections); - - const layer = renderLabelsLayer({ - id: layerId, - loader: labelsData.loader, - modelMatrix: elem.transform, - opacity: config.opacity, - visible: config.visible, - channelColors: ch?.colors && ch.colors.length > 0 ? ch.colors : labelsData.colors, - channelsVisible: - ch?.channelsVisible && ch.channelsVisible.length > 0 - ? ch.channelsVisible - : labelsData.channelsVisible, - channelOpacities: - ch?.channelOpacities && ch.channelOpacities.length > 0 - ? ch.channelOpacities - : labelsData.channelOpacities, - channelOutlineOpacities: - ch?.channelOutlineOpacities && ch.channelOutlineOpacities.length > 0 - ? ch.channelOutlineOpacities - : labelsData.channelOutlineOpacities, - channelsFilled: - ch?.channelsFilled && ch.channelsFilled.length > 0 - ? ch.channelsFilled - : labelsData.channelsFilled, - channelStrokeWidths: - ch?.channelStrokeWidths && ch.channelStrokeWidths.length > 0 - ? ch.channelStrokeWidths - : labelsData.channelStrokeWidths, - selections: stableSelections, - }); - if (layer) deckLayers.push(layer); + channelColors: ch?.colors && ch.colors.length > 0 ? ch.colors : labelsData.colors, + channelsVisible: + ch?.channelsVisible && ch.channelsVisible.length > 0 + ? ch.channelsVisible + : labelsData.channelsVisible, + channelOpacities: + ch?.channelOpacities && ch.channelOpacities.length > 0 + ? ch.channelOpacities + : labelsData.channelOpacities, + channelOutlineOpacities: + ch?.channelOutlineOpacities && ch.channelOutlineOpacities.length > 0 + ? ch.channelOutlineOpacities + : labelsData.channelOutlineOpacities, + channelsFilled: + ch?.channelsFilled && ch.channelsFilled.length > 0 + ? ch.channelsFilled + : labelsData.channelsFilled, + channelStrokeWidths: + ch?.channelStrokeWidths && ch.channelStrokeWidths.length > 0 + ? ch.channelStrokeWidths + : labelsData.channelStrokeWidths, + selections: stableSelections, + }); + if (layer) deckLayers.push(layer); + } } + // Image layers are handled separately via getVivLayerProps() } - // Image layers are handled separately via getVivLayerProps() - } - return deckLayers; - }, [layers, layerOrder, getStableSelections, pointsEngine]); + return deckLayers; + }, + [layers, layerOrder, getStableSelections, pointsEngine] + ); const getImageLayerLoadedData = useCallback((layerId: string): ImageLoaderData | undefined => { const elem = resolveLayerElement(layerId, layersRef.current[layerId], elementMap.current); - if (!elem || elem.type !== 'image') return undefined; + if (elem?.type !== 'image') return undefined; return loadedDataRef.current.images.get(elem.key); }, []); @@ -1530,7 +1536,7 @@ export function useLayerData( const getLabelsLayerLoadedData = useCallback((layerId: string): LabelsLoaderData | undefined => { const elem = resolveLayerElement(layerId, layersRef.current[layerId], elementMap.current); - if (!elem || elem.type !== 'labels') return undefined; + if (elem?.type !== 'labels') return undefined; return loadedDataRef.current.labels.get(elem.key); }, []); @@ -1646,7 +1652,7 @@ export function useLayerData( const getShapePickEvent = useCallback( (layerId: string, pickInfo: Pick<{ index?: number; object?: unknown }, 'index' | 'object'>) => { const elem = resolveLayerElement(layerId, layersRef.current[layerId], elementMap.current); - if (!elem || elem.type !== 'shapes') { + if (elem?.type !== 'shapes') { return undefined; } const feature = resolveShapeFeatureFromPick( @@ -1739,7 +1745,7 @@ export function useLayerData( if (!config?.visible || config.type !== 'image') continue; const elem = resolveLayerElement(layerId, config, elementMap.current); - if (!elem || elem.type !== 'image') continue; + if (elem?.type !== 'image') continue; const imageData = loaded.images.get(elem.key); if (!imageData) continue; // Skip if loader not ready yet diff --git a/packages/vis/src/SpatialCanvas/vivImagePassthrough.ts b/packages/vis/src/SpatialCanvas/vivImagePassthrough.ts index 25226836..dd80d43d 100644 --- a/packages/vis/src/SpatialCanvas/vivImagePassthrough.ts +++ b/packages/vis/src/SpatialCanvas/vivImagePassthrough.ts @@ -9,9 +9,7 @@ export type VivImageLayerContext = { channels: ChannelConfig; }; -export type VivImageExtensionResolver = ( - ctx: VivImageLayerContext -) => unknown[] | undefined; +export type VivImageExtensionResolver = (ctx: VivImageLayerContext) => unknown[] | undefined; export type VivImagePropsResolver = ( ctx: VivImageLayerContext diff --git a/packages/vis/src/Table/index.tsx b/packages/vis/src/Table/index.tsx index 681e0ffe..2fac6355 100644 --- a/packages/vis/src/Table/index.tsx +++ b/packages/vis/src/Table/index.tsx @@ -13,9 +13,9 @@ export default function TableComponent() { // Keep the resolved data tagged with the table it came from so stale data is // hidden by deriving during render rather than clearing via setState-in-effect. type AnnDataResult = Awaited['getAnnDataJS']>>; - const [tableData, setTableData] = useState< - { table: unknown; data: AnnDataResult } | undefined - >(undefined); + const [tableData, setTableData] = useState<{ table: unknown; data: AnnDataResult } | undefined>( + undefined + ); useEffect(() => { if (!table) return; let cancelled = false; diff --git a/packages/vis/src/Tree/index.tsx b/packages/vis/src/Tree/index.tsx index 9a25f321..8bd471c3 100644 --- a/packages/vis/src/Tree/index.tsx +++ b/packages/vis/src/Tree/index.tsx @@ -1,5 +1,5 @@ -import JsonView from '@uiw/react-json-view'; import { useSpatialData } from '@spatialdata/react'; +import JsonView from '@uiw/react-json-view'; // todo theme should adapt automatically - default light theme was illegible in dark site import { darkTheme } from '@uiw/react-json-view/dark'; diff --git a/packages/vis/src/index.ts b/packages/vis/src/index.ts index b0552858..af79d7c9 100644 --- a/packages/vis/src/index.ts +++ b/packages/vis/src/index.ts @@ -1,99 +1,97 @@ +export type { + RenderStack, + RenderStackEntry, + RenderStackGroupEntry, + RenderStackHostEntry, + RenderStackSpatialElementType, + RenderStackSpatialEntry, + SpatialLayerProps, +} from '@spatialdata/layers'; export { - SpatialLayer, getRenderStackEntryIds, getRenderStackHostLayerIds, migrateSpatialLayerProps, + RENDER_STACK_SCHEMA_VERSION, renderStackEntrySchema, renderStackGroupEntrySchema, renderStackHostEntrySchema, renderStackSchema, renderStackSpatialElementTypeSchema, renderStackSpatialEntrySchema, + SpatialLayer, spatialLayerPropsSchema, - RENDER_STACK_SCHEMA_VERSION, } from '@spatialdata/layers'; -export type { - RenderStack, - RenderStackEntry, - RenderStackGroupEntry, - RenderStackHostEntry, - RenderStackSpatialElementType, - RenderStackSpatialEntry, - SpatialLayerProps, -} from '@spatialdata/layers'; - -export { default as Sketch } from './Sketch'; -export { default as SpatialDataTree } from './Tree'; -export { default as Transforms } from './Transforms'; +export { ensureCodecWorkers } from './codecWorkers'; export { default as ImageView } from './ImageView'; export { default as Shapes } from './Shapes'; -export { default as Table } from './Table'; -export { ensureCodecWorkers } from './codecWorkers'; - +export { default as Sketch } from './Sketch'; // SpatialCanvas - composable spatial layers viewer export { default as SpatialCanvas } from './SpatialCanvas'; -export { - SpatialCanvasViewer, - SpatialCanvasProvider, - useSpatialCanvasStore, - useSpatialCanvasActions, - useSpatialCanvasStoreApi, - createSpatialCanvasStore, - useSpatialViewState, - useViewStateUrl, - SpatialViewer, - composeSpatialDeckLayers, - renderStackOrder, - renderStackToLayerInputs, - resolveRenderStackHostLayers, - shouldRenderInternalTooltip, - shouldAutoFitSpatialView, - sortLayersByRenderStackOrder, - useSpatialCanvasRenderer, - useSpatialCanvasRendererFromLayerInputs, - layerConfig, - useImageLayerContext, - ImageLayerContextProvider, - useLayerChannelState, - mergeLayerChannelState, - mergeVivImagePassthroughProps, -} from './SpatialCanvas/public'; export type { - SpatialCanvasStoreApi, - SpatialCanvasState, - SpatialCanvasActions, - SpatialCanvasStore, - ViewState, + AvailableElement, + ChannelConfig, + ElementsByType, + HoverTooltipMode, + ImageLayerContextValue, + ImageLoaderData, + LabelsSpatialFeaturePickEvent, + LayerChannelConfig, + LayerChannelDefaults, LayerConfig, LayerConfigByType, + LayerLoadState, LayerType, - ChannelConfig, - AvailableElement, - ElementsByType, + RenderStackHostLayerResolver, + RenderStackLayerInputs, + ShapesSpatialFeaturePickEvent, + SpatialCanvasActions, SpatialCanvasProps, - SpatialViewerProps, + SpatialCanvasState, + SpatialCanvasStore, + SpatialCanvasStoreApi, + SpatialCanvasTooltipRenderProps, SpatialCanvasViewerProps, SpatialCanvasViewerRenderTooltip, - HoverTooltipMode, - LabelsSpatialFeaturePickEvent, - ShapesSpatialFeaturePickEvent, SpatialFeaturePickEvent, SpatialFeatureTooltipData, SpatialFeatureTooltipItem, - SpatialFeatureTooltipSection, - SpatialCanvasTooltipRenderProps, SpatialFeatureTooltipProps, - RenderStackHostLayerResolver, - RenderStackLayerInputs, + SpatialFeatureTooltipSection, + SpatialViewerProps, UnknownRenderStackHostLayerHandler, - ImageLayerContextValue, - ImageLoaderData, - LayerLoadState, - VivImageLayerContext, + ViewState, VivImageExtensionResolver, - VivImagePropsResolver, + VivImageLayerContext, VivImagePassthroughOptions, - LayerChannelConfig, - LayerChannelDefaults, + VivImagePropsResolver, +} from './SpatialCanvas/public'; +export { + composeSpatialDeckLayers, + createSpatialCanvasStore, + ImageLayerContextProvider, + layerConfig, + mergeLayerChannelState, + mergeVivImagePassthroughProps, + renderStackOrder, + renderStackToLayerInputs, + resolveRenderStackHostLayers, + SpatialCanvasProvider, + SpatialCanvasViewer, + SpatialFeatureTooltip, + SpatialViewer, + shouldAutoFitSpatialView, + shouldRenderInternalTooltip, + sortLayersByRenderStackOrder, + useImageLayerContext, + useLayerChannelState, + useSpatialCanvasActions, + useSpatialCanvasRenderer, + useSpatialCanvasRendererFromLayerInputs, + useSpatialCanvasStore, + useSpatialCanvasStoreApi, + useSpatialViewState, + useViewStateUrl, } from './SpatialCanvas/public'; -export { SpatialFeatureTooltip } from './SpatialCanvas/public'; +export { default as Table } from './Table'; +export { default as Transforms } from './Transforms'; +export { default as SpatialDataTree } from './Tree'; diff --git a/packages/vis/tsconfig.json b/packages/vis/tsconfig.json index bf8b935b..63fe609b 100644 --- a/packages/vis/tsconfig.json +++ b/packages/vis/tsconfig.json @@ -13,7 +13,7 @@ "skipLibCheck": true, "esModuleInterop": true, "resolveJsonModule": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ES2022", "DOM", "DOM.Iterable"], // Cross-package types resolve via node_modules to each package's built dist/*.d.ts // (topological build order guarantees deps are built first). Matches core's pattern // and keeps emitted .d.ts referencing bare '@spatialdata/*' specifiers. diff --git a/packages/zarrextra/package.json b/packages/zarrextra/package.json index 4d4c47b8..a9392ad4 100644 --- a/packages/zarrextra/package.json +++ b/packages/zarrextra/package.json @@ -8,14 +8,14 @@ "exports": { ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.js" + "default": "./dist/index.js" }, "./workers": { "types": "./dist/workers/index.d.ts", - "import": "./dist/workers.js" + "default": "./dist/workers.js" }, "./codec-worker": { - "import": "./dist/codec-worker.js" + "default": "./dist/codec-worker.js" } }, "files": [ @@ -25,8 +25,8 @@ "access": "public" }, "scripts": { - "build": "vite build && vite build --mode codec-worker && tsc --emitDeclarationOnly --outDir dist", - "dev": "vite build --watch", + "build": "rm -rf dist && vite build && vite build --mode codec-worker && tsc --emitDeclarationOnly --outDir dist", + "dev": "vite build --mode codec-worker && vite build --watch", "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage" diff --git a/packages/zarrextra/src/chunkDecode.ts b/packages/zarrextra/src/chunkDecode.ts index 87c5d891..07045cd5 100644 --- a/packages/zarrextra/src/chunkDecode.ts +++ b/packages/zarrextra/src/chunkDecode.ts @@ -1,6 +1,6 @@ -import * as zarr from 'zarrita'; +import type { GetWorkerOptions } from '@fideus-labs/fizarrita'; import type { WorkerPool } from '@fideus-labs/worker-pool'; -import type { ChunkCache, GetWorkerOptions } from '@fideus-labs/fizarrita'; +import * as zarr from 'zarrita'; export type ZarrGetOptions = { signal?: AbortSignal; diff --git a/packages/zarrextra/src/index.ts b/packages/zarrextra/src/index.ts index cea337f3..2a024a13 100644 --- a/packages/zarrextra/src/index.ts +++ b/packages/zarrextra/src/index.ts @@ -182,7 +182,7 @@ export function serializeZarrTree(obj: ZarrTree | unknown): unknown { } for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { + if (Object.hasOwn(obj, key)) { // @ts-expect-error - Indexing unknown object for serialization. const val = obj[key]; if (typeof val === 'function') { @@ -196,45 +196,44 @@ export function serializeZarrTree(obj: ZarrTree | unknown): unknown { return result; } -export type { - StoreReference, - ZarrTree, - ConsolidatedStore, - LazyZarrArray, - ZAttrsAny, -} from './types'; -export { ATTRS_KEY, ZARRAY_KEY } from './types'; -export { createPrefixedStore } from './prefixedStore'; export { createOpenJpegDecoder, createOpenJphDecoder, createWasmLocateFile, - registerJpeg2kCodec, - registerExperimentalHtj2kCodec, type ImageCodecDecoder, type OpenJpegFactory, type OpenJphDecode, type OpenJphDecodedImage, type OpenJphInitOptions, type RegisterImageCodecOptions, + registerExperimentalHtj2kCodec, + registerJpeg2kCodec, } from './codecs'; export { createOpenJphEncoder, encodeHtj2kPlane, - loadOpenJphEncoder, - planeArrayForDtype, type Htj2kEncodeOptions, type Htj2kPlane, type Htj2kPlaneDtype, + loadOpenJphEncoder, type OpenJphEncode, type OpenJphEncodeInput, type OpenJphEncoder, + planeArrayForDtype, } from './htj2k-encode'; export { loadOmeZarrMultiscalesFromStore, - type VivCompatiblePixelSource, type RasterSelection, + type VivCompatiblePixelSource, } from './omeZarr'; - +export { createPrefixedStore } from './prefixedStore'; export type { Result } from './result'; -export { Ok, Err, isOk, isErr, unwrap, unwrapOr } from './result'; +export { Err, isErr, isOk, Ok, unwrap, unwrapOr } from './result'; +export type { + ConsolidatedStore, + LazyZarrArray, + StoreReference, + ZAttrsAny, + ZarrTree, +} from './types'; +export { ATTRS_KEY, ZARRAY_KEY } from './types'; diff --git a/packages/zarrextra/src/omeZarr.ts b/packages/zarrextra/src/omeZarr.ts index 96843077..5759a021 100644 --- a/packages/zarrextra/src/omeZarr.ts +++ b/packages/zarrextra/src/omeZarr.ts @@ -253,11 +253,7 @@ class ZarrPixelSource implements VivCompatiblePixelSource { async getRaster({ selection, signal }: { selection: RasterSelection; signal?: AbortSignal }) { const sel = this.chunkIndex(selection, { x: null, y: null }); const result = await this.getRaw(sel, signal); - const [height, width] = spatialDimensionsFromChunk( - result.shape, - this.data.shape, - this.labels - ); + const [height, width] = spatialDimensionsFromChunk(result.shape, this.data.shape, this.labels); return { data: result.data, width, height }; } @@ -275,11 +271,7 @@ class ZarrPixelSource implements VivCompatiblePixelSource { const [xSlice, ySlice] = this.getSlices(x, y); const sel = this.chunkIndex(selection, { x: xSlice, y: ySlice }); const result = await this.getRaw(sel, signal); - const [height, width] = spatialDimensionsFromChunk( - result.shape, - this.data.shape, - this.labels - ); + const [height, width] = spatialDimensionsFromChunk(result.shape, this.data.shape, this.labels); return { data: result.data, width, height }; } diff --git a/packages/zarrextra/src/workers/codec-worker-init.ts b/packages/zarrextra/src/workers/codec-worker-init.ts index b305d5a4..424b7e76 100644 --- a/packages/zarrextra/src/workers/codec-worker-init.ts +++ b/packages/zarrextra/src/workers/codec-worker-init.ts @@ -1,7 +1,7 @@ -import { decode as openJphDecode } from 'openjph-wasm'; -import openJphWasmUrl from 'openjph-wasm/wasm/libopenjph.wasm?url'; import OpenJPEGJS from '@cornerstonejs/codec-openjpeg/decode'; import openJpegWasmUrl from '@cornerstonejs/codec-openjpeg/decodewasm?url'; +import { decode as openJphDecode } from 'openjph-wasm'; +import openJphWasmUrl from 'openjph-wasm/wasm/libopenjph.wasm?url'; import { createOpenJpegDecoder, createOpenJphDecoder, diff --git a/packages/zarrextra/src/workers/workerControls.ts b/packages/zarrextra/src/workers/workerControls.ts index cd5ab2b0..647237e7 100644 --- a/packages/zarrextra/src/workers/workerControls.ts +++ b/packages/zarrextra/src/workers/workerControls.ts @@ -1,5 +1,5 @@ +import { type ChunkCache, type GetWorkerOptions, getWorker } from '@fideus-labs/fizarrita'; import { WorkerPool } from '@fideus-labs/worker-pool'; -import { getWorker, type ChunkCache, type GetWorkerOptions } from '@fideus-labs/fizarrita'; import { setChunkDecodeBackend, setFizarritaGetWorker } from '../chunkDecode'; export type { ChunkCache } from '@fideus-labs/fizarrita'; diff --git a/packages/zarrextra/tsconfig.json b/packages/zarrextra/tsconfig.json index 394538c2..5f4a1950 100644 --- a/packages/zarrextra/tsconfig.json +++ b/packages/zarrextra/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "ES2020", "module": "ESNext", - "lib": ["ES2020"], + "lib": ["ES2022"], // zarrextra's own source uses only universal Web-standard globals (AbortSignal, // DOMException, TextDecoder, URL, and a guarded `navigator`), all present in Node // 18+ as well as browsers/workers — so we type against Node's globals rather than diff --git a/packages/zarrextra/vite.config.ts b/packages/zarrextra/vite.config.ts index 0a3fd22f..127083d2 100644 --- a/packages/zarrextra/vite.config.ts +++ b/packages/zarrextra/vite.config.ts @@ -9,7 +9,12 @@ export default defineConfig(({ mode }) => { return { build: { - emptyOutDir: !isCodecWorkerBuild, + // Never empty dist here. `codec-worker.js` is emitted by a separate pass + // (--mode codec-worker), so a default pass that empties dist would delete it + // and leave `workers.js`'s `new URL('./codec-worker.js', ...)` dangling — + // which is exactly what `dev` (vite build --watch) used to do. The `build` + // script does an explicit `rm -rf dist` instead. + emptyOutDir: false, lib: { entry: isCodecWorkerBuild ? resolve(__dirname, 'src/workers/codec-worker.ts')