From 309c9ddd5f3fb255e441994cf07624783fe7afab Mon Sep 17 00:00:00 2001 From: noah Date: Tue, 5 May 2026 14:22:02 -0700 Subject: [PATCH 1/3] update the cache-client to support isValue that is aware of the request object - this makes it easier to build cache clients that are more flexible, which is nice for scatterbrain, as it is often the case that we rebuild a shader to bind different vertex buffers, but we'd like to keep using the same cache-client, as almost all the data is already there... --- packages/core/package.json | 4 ++-- packages/core/src/shared-priority-cache/shared-cache.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index b4ac9b30..8bcb7373 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@alleninstitute/vis-core", - "version": "0.0.6", + "version": "0.0.7", "contributors": [ { "name": "Lane Sawyer", @@ -61,4 +61,4 @@ "uuid": "13.0.0" }, "packageManager": "pnpm@9.14.2" -} +} \ No newline at end of file diff --git a/packages/core/src/shared-priority-cache/shared-cache.ts b/packages/core/src/shared-priority-cache/shared-cache.ts index 0df0f32f..16e52b42 100644 --- a/packages/core/src/shared-priority-cache/shared-cache.ts +++ b/packages/core/src/shared-priority-cache/shared-cache.ts @@ -19,7 +19,7 @@ type CacheInterface> = { }; export type ClientSpec> = { - isValue: (v: Record) => v is ItemContent; + isValue: (v: Record, item: Item) => v is ItemContent; cacheKeys: (item: Item) => { [k in keyof ItemContent]: string }; onDataArrived?: (cacheKey: string, result: FetchResult) => void; fetch: (item: Item) => { [k in keyof ItemContent]: (abort: AbortSignal) => Promise }; @@ -46,8 +46,8 @@ function mapFields, Result>( type Client = { priorities: Record; notify: - | undefined - | ((cacheKey: string, result: { status: 'success' } | { status: 'failure'; reason: unknown }) => void); + | undefined + | ((cacheKey: string, result: { status: 'success' } | { status: 'failure'; reason: unknown }) => void); }; export class SharedPriorityCache { private cache: AsyncPriorityCache; @@ -109,7 +109,7 @@ export class SharedPriorityCache { get: (k: Item) => { const keys = spec.cacheKeys(k); const v = mapFields, Cacheable | undefined>(keys, (k) => this.cache.get(k)); - return spec.isValue(v) ? v : undefined; + return spec.isValue(v, k) ? v : undefined; }, has: (k: Item) => { const atLeastOneMissing = Object.values(spec.cacheKeys(k)).some((ck) => !this.cache.has(ck)); From 227b39439e37b9c9a026a3c7bf1256fb2582a718 Mon Sep 17 00:00:00 2001 From: noah Date: Tue, 5 May 2026 14:22:34 -0700 Subject: [PATCH 2/3] update scatterbrain cache client, make it more generic (could handle either webGL or WebGPU easily) --- packages/scatterbrain/package.json | 4 +- packages/scatterbrain/src/cache-client.ts | 65 +++++++++++++++++ packages/scatterbrain/src/index.ts | 6 +- packages/scatterbrain/src/renderer.ts | 88 +++++------------------ packages/scatterbrain/src/types.ts | 13 ++++ 5 files changed, 99 insertions(+), 77 deletions(-) create mode 100644 packages/scatterbrain/src/cache-client.ts diff --git a/packages/scatterbrain/package.json b/packages/scatterbrain/package.json index 28b5e4a5..9de4eab3 100644 --- a/packages/scatterbrain/package.json +++ b/packages/scatterbrain/package.json @@ -1,6 +1,6 @@ { "name": "@alleninstitute/vis-scatterbrain", - "version": "0.0.2", + "version": "0.0.3", "contributors": [ { "name": "Lane Sawyer", @@ -65,4 +65,4 @@ "devDependencies": { "@types/lodash": "4.17.24" } -} +} \ No newline at end of file diff --git a/packages/scatterbrain/src/cache-client.ts b/packages/scatterbrain/src/cache-client.ts new file mode 100644 index 00000000..1283ac17 --- /dev/null +++ b/packages/scatterbrain/src/cache-client.ts @@ -0,0 +1,65 @@ +import type { Cacheable, SharedPriorityCache } from '@alleninstitute/vis-core'; +import reduce from 'lodash/reduce'; +import type { WebGLSafeBasicType } from './typed-array'; +import type { ColumnRequest, Item, } from './types'; + + +type Content = Record + +export function buildScatterbrainCacheClient( + cache: SharedPriorityCache, + toCacheValue: (buffer: ArrayBuffer, type: WebGLSafeBasicType) => V, + onDataArrived: () => void, +) { + const client = cache.registerClient>({ + cacheKeys: (item) => { + const { dataset, node, columns } = item; + return reduce, Record>( + columns, + (acc, col, key) => ({ + ...acc, + [key]: `${dataset.metadata.metadataFileEndpoint}/${node.file}/${col.name}`, + }), + {}, + ); + }, + fetch: (item) => { + const { dataset, node, columns } = item; + const attrs = dataset.metadata.pointAttributes; + const getColumnUrl = (columnName: string) => + `${dataset.metadata.metadataFileEndpoint}${columnName}/${dataset.metadata.visualizationReferenceId}/${node.file}`; + const getGeneUrl = (columnName: string) => + `${dataset.metadata.geneFileEndpoint}${columnName}/${dataset.metadata.visualizationReferenceId}/${node.file}`; + const getColumnInfo = (col: ColumnRequest) => + col.type === 'QUANTITATIVE' + ? ({ url: getGeneUrl(col.name), elements: 1, type: 'float' } as const) + : { url: getColumnUrl(col.name), elements: attrs[col.name].elements, type: attrs[col.name].type }; + + const proms = reduce, Record Promise>>( + columns, + (getters, col, key) => { + const { url, type } = getColumnInfo(col); + return { + ...getters, + [key]: (signal) => + fetch(url, { signal }).then((b) => + b.arrayBuffer().then((buff) => toCacheValue(buff, type)) + ), + }; + }, + {}, + ); + return proms; + }, + isValue: (v, item: Item): v is Content => { + for (const column of Object.keys(item.columns)) { + if (!(column in v)) { + return false; + } + } + return true; + }, + onDataArrived, + }); + return client; +} \ No newline at end of file diff --git a/packages/scatterbrain/src/index.ts b/packages/scatterbrain/src/index.ts index bcc39ea3..d246bc5e 100644 --- a/packages/scatterbrain/src/index.ts +++ b/packages/scatterbrain/src/index.ts @@ -1,8 +1,8 @@ +export { buildScatterbrainCacheClient } from './cache-client'; +export { getVisibleItems, loadDataset as loadScatterbrainDataset } from './dataset'; export { buildRenderFrameFn as buildScatterbrainRenderFn, - buildScatterbrainCacheClient, setCategoricalLookupTableValues, - updateCategoricalValue, + updateCategoricalValue } from './renderer'; export * from './types'; -export { getVisibleItems, loadDataset as loadScatterbrainDataset } from './dataset'; diff --git a/packages/scatterbrain/src/renderer.ts b/packages/scatterbrain/src/renderer.ts index 10a9e983..b8302c7b 100644 --- a/packages/scatterbrain/src/renderer.ts +++ b/packages/scatterbrain/src/renderer.ts @@ -1,85 +1,21 @@ import type { SharedPriorityCache } from '@alleninstitute/vis-core'; -import type REGL from 'regl'; -import type { ColumnRequest, ScatterbrainDataset, SlideviewScatterbrainDataset, TreeNode } from './types'; import { Box2D, type box2D, type vec4 } from '@alleninstitute/vis-geometry'; -import { MakeTaggedBufferView } from './typed-array'; import keys from 'lodash/keys'; import reduce from 'lodash/reduce'; +import type REGL from 'regl'; +import { buildScatterbrainCacheClient } from './cache-client'; import { getVisibleItems, type NodeWithBounds } from './dataset'; import { buildScatterbrainRenderCommand, type Config, configureShader, type ShaderSettings, VBO } from './shader'; +import { MakeTaggedBufferView } from './typed-array'; +import type { ColumnRequest, ScatterbrainDataset, SlideviewScatterbrainDataset, TreeNode } from './types'; + export type Item = Readonly<{ dataset: SlideviewScatterbrainDataset | ScatterbrainDataset; node: TreeNode; bounds: box2D; columns: Record; }>; -type Content = Record; - -export function buildScatterbrainCacheClient( - allNeededColumns: readonly string[], - regl: REGL.Regl, - cache: SharedPriorityCache, - onDataArrived: () => void, -) { - const client = cache.registerClient({ - cacheKeys: (item) => { - const { dataset, node, columns } = item; - return reduce, Record>( - columns, - (acc, col, key) => ({ - ...acc, - [key]: `${dataset.metadata.metadataFileEndpoint}/${node.file}/${col.name}`, - }), - {}, - ); - }, - fetch: (item) => { - const { dataset, node, columns } = item; - const attrs = dataset.metadata.pointAttributes; - const getColumnUrl = (columnName: string) => - `${dataset.metadata.metadataFileEndpoint}${columnName}/${dataset.metadata.visualizationReferenceId}/${node.file}`; - const getGeneUrl = (columnName: string) => - `${dataset.metadata.geneFileEndpoint}${columnName}/${dataset.metadata.visualizationReferenceId}/${node.file}`; - const getColumnInfo = (col: ColumnRequest) => - col.type === 'QUANTITATIVE' - ? ({ url: getGeneUrl(col.name), elements: 1, type: 'float' } as const) - : { url: getColumnUrl(col.name), elements: attrs[col.name].elements, type: attrs[col.name].type }; - const proms = reduce, Record Promise>>( - columns, - (getters, col, key) => { - const { url, type } = getColumnInfo(col); - return { - ...getters, - [key]: (signal) => - fetch(url, { signal }).then((b) => - b.arrayBuffer().then((buff) => { - const typed = MakeTaggedBufferView(type, buff); - return new VBO({ - buffer: regl.buffer({ type: type, data: typed.data }), - bytes: buff.byteLength, - type: 'buffer', - }); - }), - ), - }; - }, - {}, - ); - return proms; - }, - isValue: (v): v is Content => { - for (const column of allNeededColumns) { - if (!(column in v)) { - return false; - } - } - return true; - }, - onDataArrived, - }); - return client; -} function columnsForItem( config: Config, @@ -175,7 +111,7 @@ export function updateCategoricalValue( type ScatterbrainRenderProps = Omit>[0], 'item'> & { visibilityThresholdPx: number; dataset: ScatterbrainDataset | SlideviewScatterbrainDataset; - client: ReturnType; + client: ReturnType>; }; /** * @@ -214,8 +150,16 @@ export function buildRenderFrameFn(regl: REGL.Regl, settings: ShaderSettings) { } }; const connectToCache = (cache: SharedPriorityCache, onDataArrived: () => void) => { - const allColumns = [...config.categoricalColumns, ...config.quantitativeColumns, config.positionColumn]; - const client = buildScatterbrainCacheClient(allColumns, regl, cache, onDataArrived); + const client = buildScatterbrainCacheClient(cache, + (buff, type) => { + const typed = MakeTaggedBufferView(type, buff); + return new VBO({ + buffer: regl.buffer({ type: type, data: typed.data }), + bytes: buff.byteLength, + type: 'buffer', + }); + }, + onDataArrived); return client; }; return { render, connectToCache }; diff --git a/packages/scatterbrain/src/types.ts b/packages/scatterbrain/src/types.ts index 50a442ec..c1b6fa39 100644 --- a/packages/scatterbrain/src/types.ts +++ b/packages/scatterbrain/src/types.ts @@ -1,5 +1,8 @@ /// Types describing the metadata that gets loaded from scatterbrain.json files /// // there are 2 variants, slideview and regular - they are distinguished at runtime + +import type { box2D } from "@alleninstitute/vis-geometry"; + // by checking the parsed metadata for the 'slides' field export type WebGLSafeBasicType = 'uint8' | 'uint16' | 'int8' | 'int16' | 'uint32' | 'int32' | 'float'; @@ -82,3 +85,13 @@ export type SlideviewMetadata = CommonMetadata & { export type SlideviewScatterbrainDataset = { type: 'slideview'; metadata: SlideviewMetadata }; export type ScatterbrainDataset = { type: 'normal'; metadata: ScatterbrainMetadata }; + + +// renderer-specific types: + +export type Item = Readonly<{ + dataset: SlideviewScatterbrainDataset | ScatterbrainDataset; + node: TreeNode; + bounds: box2D; + columns: Record; +}>; \ No newline at end of file From a1ac34bc1c298a94686358ceaf6032f8085272e7 Mon Sep 17 00:00:00 2001 From: noah Date: Tue, 5 May 2026 14:33:07 -0700 Subject: [PATCH 3/3] fmt --- packages/core/package.json | 2 +- packages/core/src/shared-priority-cache/shared-cache.ts | 4 ++-- packages/scatterbrain/package.json | 2 +- packages/scatterbrain/src/cache-client.ts | 9 ++++----- packages/scatterbrain/src/index.ts | 2 +- packages/scatterbrain/src/renderer.ts | 7 ++++--- packages/scatterbrain/src/types.ts | 5 ++--- 7 files changed, 15 insertions(+), 16 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 8bcb7373..8f8b5527 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -61,4 +61,4 @@ "uuid": "13.0.0" }, "packageManager": "pnpm@9.14.2" -} \ No newline at end of file +} diff --git a/packages/core/src/shared-priority-cache/shared-cache.ts b/packages/core/src/shared-priority-cache/shared-cache.ts index 16e52b42..46d4d33d 100644 --- a/packages/core/src/shared-priority-cache/shared-cache.ts +++ b/packages/core/src/shared-priority-cache/shared-cache.ts @@ -46,8 +46,8 @@ function mapFields, Result>( type Client = { priorities: Record; notify: - | undefined - | ((cacheKey: string, result: { status: 'success' } | { status: 'failure'; reason: unknown }) => void); + | undefined + | ((cacheKey: string, result: { status: 'success' } | { status: 'failure'; reason: unknown }) => void); }; export class SharedPriorityCache { private cache: AsyncPriorityCache; diff --git a/packages/scatterbrain/package.json b/packages/scatterbrain/package.json index 9de4eab3..dfa0abb1 100644 --- a/packages/scatterbrain/package.json +++ b/packages/scatterbrain/package.json @@ -65,4 +65,4 @@ "devDependencies": { "@types/lodash": "4.17.24" } -} \ No newline at end of file +} diff --git a/packages/scatterbrain/src/cache-client.ts b/packages/scatterbrain/src/cache-client.ts index 1283ac17..dddadbce 100644 --- a/packages/scatterbrain/src/cache-client.ts +++ b/packages/scatterbrain/src/cache-client.ts @@ -1,10 +1,9 @@ import type { Cacheable, SharedPriorityCache } from '@alleninstitute/vis-core'; import reduce from 'lodash/reduce'; import type { WebGLSafeBasicType } from './typed-array'; -import type { ColumnRequest, Item, } from './types'; +import type { ColumnRequest, Item } from './types'; - -type Content = Record +type Content = Record; export function buildScatterbrainCacheClient( cache: SharedPriorityCache, @@ -43,7 +42,7 @@ export function buildScatterbrainCacheClient( ...getters, [key]: (signal) => fetch(url, { signal }).then((b) => - b.arrayBuffer().then((buff) => toCacheValue(buff, type)) + b.arrayBuffer().then((buff) => toCacheValue(buff, type)), ), }; }, @@ -62,4 +61,4 @@ export function buildScatterbrainCacheClient( onDataArrived, }); return client; -} \ No newline at end of file +} diff --git a/packages/scatterbrain/src/index.ts b/packages/scatterbrain/src/index.ts index d246bc5e..ebdd683f 100644 --- a/packages/scatterbrain/src/index.ts +++ b/packages/scatterbrain/src/index.ts @@ -3,6 +3,6 @@ export { getVisibleItems, loadDataset as loadScatterbrainDataset } from './datas export { buildRenderFrameFn as buildScatterbrainRenderFn, setCategoricalLookupTableValues, - updateCategoricalValue + updateCategoricalValue, } from './renderer'; export * from './types'; diff --git a/packages/scatterbrain/src/renderer.ts b/packages/scatterbrain/src/renderer.ts index b8302c7b..539a6f79 100644 --- a/packages/scatterbrain/src/renderer.ts +++ b/packages/scatterbrain/src/renderer.ts @@ -16,7 +16,6 @@ export type Item = Readonly<{ columns: Record; }>; - function columnsForItem( config: Config, col2shader: Record, @@ -150,7 +149,8 @@ export function buildRenderFrameFn(regl: REGL.Regl, settings: ShaderSettings) { } }; const connectToCache = (cache: SharedPriorityCache, onDataArrived: () => void) => { - const client = buildScatterbrainCacheClient(cache, + const client = buildScatterbrainCacheClient( + cache, (buff, type) => { const typed = MakeTaggedBufferView(type, buff); return new VBO({ @@ -159,7 +159,8 @@ export function buildRenderFrameFn(regl: REGL.Regl, settings: ShaderSettings) { type: 'buffer', }); }, - onDataArrived); + onDataArrived, + ); return client; }; return { render, connectToCache }; diff --git a/packages/scatterbrain/src/types.ts b/packages/scatterbrain/src/types.ts index c1b6fa39..21782531 100644 --- a/packages/scatterbrain/src/types.ts +++ b/packages/scatterbrain/src/types.ts @@ -1,7 +1,7 @@ /// Types describing the metadata that gets loaded from scatterbrain.json files /// // there are 2 variants, slideview and regular - they are distinguished at runtime -import type { box2D } from "@alleninstitute/vis-geometry"; +import type { box2D } from '@alleninstitute/vis-geometry'; // by checking the parsed metadata for the 'slides' field export type WebGLSafeBasicType = 'uint8' | 'uint16' | 'int8' | 'int16' | 'uint32' | 'int32' | 'float'; @@ -86,7 +86,6 @@ export type SlideviewMetadata = CommonMetadata & { export type SlideviewScatterbrainDataset = { type: 'slideview'; metadata: SlideviewMetadata }; export type ScatterbrainDataset = { type: 'normal'; metadata: ScatterbrainMetadata }; - // renderer-specific types: export type Item = Readonly<{ @@ -94,4 +93,4 @@ export type Item = Readonly<{ node: TreeNode; bounds: box2D; columns: Record; -}>; \ No newline at end of file +}>;