@@ -71,7 +71,7 @@ export class LabelLayer extends TileLayer<LabelPixelData> {
71
71
pixelData : data ,
72
72
opacity : props . opacity ,
73
73
modelMatrix : props . modelMatrix ,
74
- lut : createColorLookupTable ( { source : props . lut } ) ,
74
+ lut : createColorLookupTable ( { source : props . lut , fallback : DEFAULT_PALETTE } ) ,
75
75
bounds : [ clamp ( left , 0 , width ) , clamp ( top , 0 , height ) , clamp ( right , 0 , width ) , clamp ( bottom , 0 , height ) ] ,
76
76
// For underlying class
77
77
image : new ImageData ( data . width , data . height ) ,
@@ -192,27 +192,44 @@ function getTileSizeForResolutions(resolutions: Array<ZarrPixelSource>): number
192
192
return tileSize ;
193
193
}
194
194
195
+ // Approx. Limit for WebGL2
196
+ const MAX_UNIFORM_VEC3 = 1365 ;
197
+ const SEEN_LUTS = new WeakSet < LabelLayerLut > ( ) ;
198
+
199
+ /**
200
+ * Creates a color lookup table (LUT) as a Float32Array.
201
+ * Falls back to a default LUT if the source is missing or exceeds the uniform size limit.
202
+ *
203
+ * @param options.source - The source lookup table.
204
+ * @param options.fallback - The fallback LUT to use if the source is invalid.
205
+ * @returns {Float32Array | undefined } The generated LUT or the fallback if the source is too large.
206
+ */
195
207
function createColorLookupTable ( options : {
196
208
source ?: LabelLayerLut ;
197
- palette ?: ReadonlyArray < readonly [ number , number , number ] > ;
209
+ fallback : Float32Array ;
198
210
} ) : Float32Array {
199
- const { source, palette = COLOR_PALETTES . pathology } = options ;
200
- if ( source ) {
201
- const values = Object . keys ( source ) . map ( ( value ) => + value ) ;
202
- // This could blow up in size, should we worry about it?
203
- // What about alpha?
204
- const lut = new Float32Array ( Math . max ( ...values ) * 3 ) ;
205
- for ( let [ value , color ] of Object . entries ( source ) ) {
206
- const i = + value ;
207
- lut [ i * 3 + 0 ] = color [ 0 ] ;
208
- lut [ i * 3 + 1 ] = color [ 1 ] ;
209
- lut [ i * 3 + 2 ] = color [ 2 ] ;
211
+ const { source, fallback } = options ;
212
+ if ( ! source ) {
213
+ return fallback ;
214
+ }
215
+ const values = Object . keys ( source ) . map ( ( value ) => + value ) ;
216
+ const size = Math . max ( ...values ) ;
217
+ if ( size >= MAX_UNIFORM_VEC3 ) {
218
+ if ( ! SEEN_LUTS . has ( source ) ) {
219
+ console . warn ( "[vizarr] Skipping color palette from OME-NGFF `image-label` source: exceeds uniform size limit." ) ;
220
+ SEEN_LUTS . add ( source ) ;
210
221
}
211
- return lut ;
222
+ return fallback ;
212
223
}
213
-
214
- // generate a random categorical palette
215
- return Float32Array . from ( palette . flat ( ) , ( d ) => d / 255.0 ) ;
224
+ // Either empty source or larger than what we can handle with WebGL uniforms
225
+ const lut = new Float32Array ( size * 3 ) ;
226
+ for ( let [ value , color ] of Object . entries ( source ) ) {
227
+ const i = + value ;
228
+ lut [ i * 3 + 0 ] = color [ 0 ] ;
229
+ lut [ i * 3 + 1 ] = color [ 1 ] ;
230
+ lut [ i * 3 + 2 ] = color [ 2 ] ;
231
+ }
232
+ return lut ;
216
233
}
217
234
218
235
const COLOR_PALETTES = {
@@ -267,6 +284,8 @@ const COLOR_PALETTES = {
267
284
] ,
268
285
} as const ;
269
286
287
+ const DEFAULT_PALETTE = Float32Array . from ( COLOR_PALETTES . pathology . flat ( ) , ( d ) => d / 255 ) ;
288
+
270
289
function typedArrayConstructorName ( arr : zarr . TypedArray < LabelDataType > ) {
271
290
const ArrayType = arr . constructor as zarr . TypedArrayConstructor < LabelDataType > ;
272
291
const name = ArrayType . name as `${Capitalize < LabelDataType > } Array`;
0 commit comments