@tissuumaps/core 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3091 @@
1
+ import { rgb } from "d3-color";
2
+ import { mat3, vec2 } from "gl-matrix";
3
+ import { interpolateBlues, interpolateBrBG, interpolateBuGn, interpolateBuPu, interpolateCividis, interpolateCool, interpolateCubehelixDefault, interpolateGnBu, interpolateGreens, interpolateGreys, interpolateInferno, interpolateMagma, interpolateOrRd, interpolateOranges, interpolatePRGn, interpolatePiYG, interpolatePlasma, interpolatePuBu, interpolatePuBuGn, interpolatePuOr, interpolatePuRd, interpolatePurples, interpolateRainbow, interpolateRdBu, interpolateRdGy, interpolateRdPu, interpolateRdYlBu, interpolateRdYlGn, interpolateReds, interpolateSinebow, interpolateSpectral, interpolateTurbo, interpolateViridis, interpolateWarm, interpolateYlGn, interpolateYlGnBu, interpolateYlOrBr, interpolateYlOrRd, schemeAccent, schemeCategory10, schemeDark2, schemeObservable10, schemePaired, schemePastel1, schemePastel2, schemeSet1, schemeSet2, schemeSet3, schemeTableau10 } from "d3-scale-chromatic";
4
+ //#region src/model/primitives.ts
5
+ /** The marker shapes that points can be rendered as */
6
+ var Marker = {
7
+ Cross: 0,
8
+ Diamond: 1,
9
+ Square: 2,
10
+ TriangleUp: 3,
11
+ Star: 4,
12
+ Clobber: 5,
13
+ Disc: 6,
14
+ HBar: 7,
15
+ VBar: 8,
16
+ TailedArrow: 9,
17
+ TriangleDown: 10,
18
+ Ring: 11,
19
+ X: 12,
20
+ Arrow: 13,
21
+ Gaussian: 14
22
+ };
23
+ //#endregion
24
+ //#region src/model/constants.ts
25
+ /** Identity similarity transform */
26
+ var identityTransform = {
27
+ flip: false,
28
+ scale: 1,
29
+ rotation: 0,
30
+ translation: {
31
+ x: 0,
32
+ y: 0
33
+ }
34
+ };
35
+ /**
36
+ * Default seed of random configurations (see `RandomConfig`), used when a
37
+ * configuration does not specify one
38
+ */
39
+ var defaultRandomSeed = 0;
40
+ /** Default label color */
41
+ var defaultLabelColor = {
42
+ r: 255,
43
+ g: 255,
44
+ b: 255
45
+ };
46
+ /** ID of the default color palette for random label colors */
47
+ var defaultLabelColorPalette = "distinct";
48
+ /** Default label visibility */
49
+ var defaultLabelVisibility = true;
50
+ /** Default label opacity */
51
+ var defaultLabelOpacity = 1;
52
+ /** Default point marker */
53
+ var defaultPointMarker = Marker.Disc;
54
+ /** Default point size */
55
+ var defaultPointSize = 1;
56
+ /** Default coordinate space in which point sizes are specified */
57
+ var defaultPointSizeUnit = "data";
58
+ /** Default point color */
59
+ var defaultPointColor = {
60
+ r: 255,
61
+ g: 255,
62
+ b: 255
63
+ };
64
+ /** Default point visibility */
65
+ var defaultPointVisibility = true;
66
+ /** Default point opacity */
67
+ var defaultPointOpacity = 1;
68
+ /** Default shape visibility */
69
+ var defaultShapeVisibility = true;
70
+ /** Default shape opacity */
71
+ var defaultShapeOpacity = 1;
72
+ /** Default shape fill color */
73
+ var defaultShapeFillColor = {
74
+ r: 255,
75
+ g: 255,
76
+ b: 255
77
+ };
78
+ /** Default shape fill visibility */
79
+ var defaultShapeFillVisibility = true;
80
+ /** Default shape fill opacity */
81
+ var defaultShapeFillOpacity = 1;
82
+ /** Default shape stroke color */
83
+ var defaultShapeStrokeColor = {
84
+ r: 0,
85
+ g: 0,
86
+ b: 0
87
+ };
88
+ /** Default shape stroke visibility */
89
+ var defaultShapeStrokeVisibility = true;
90
+ /** Default shape stroke opacity */
91
+ var defaultShapeStrokeOpacity = 1;
92
+ //#endregion
93
+ //#region src/model/base.ts
94
+ /**
95
+ * Default values for {@link RawModel}
96
+ */
97
+ var modelDefaults = {};
98
+ /**
99
+ * Creates a {@link Model} from a {@link RawModel} by applying {@link modelDefaults}
100
+ *
101
+ * @param rawModel - The raw model
102
+ * @returns The complete model with default values applied
103
+ */
104
+ function createModel(rawModel) {
105
+ return {
106
+ ...structuredClone(modelDefaults),
107
+ ...structuredClone(rawModel)
108
+ };
109
+ }
110
+ /**
111
+ * Default values for {@link RawDataObject}
112
+ */
113
+ var dataObjectDefaults = {};
114
+ /**
115
+ * Creates a {@link DataObject} from a {@link RawDataObject} by applying {@link dataObjectDefaults}
116
+ *
117
+ * @param rawDataObject - The raw data object
118
+ * @returns The complete data object with default values applied
119
+ */
120
+ function createDataObject(rawDataObject) {
121
+ return {
122
+ ...createModel(rawDataObject),
123
+ ...structuredClone(dataObjectDefaults),
124
+ ...structuredClone(rawDataObject),
125
+ dataSource: createDataSource(rawDataObject.dataSource)
126
+ };
127
+ }
128
+ /**
129
+ * Default values for {@link RawRenderedDataObject}
130
+ */
131
+ var renderedDataObjectDefaults = {
132
+ visibility: true,
133
+ opacity: 1,
134
+ transform: identityTransform
135
+ };
136
+ /**
137
+ * Creates a {@link RenderedDataObject} from a {@link RawRenderedDataObject} by applying {@link renderedDataObjectDefaults}
138
+ *
139
+ * @param rawRenderedDataObject - The raw rendered data object
140
+ * @returns The complete rendered data object with default values applied
141
+ */
142
+ function createRenderedDataObject(rawRenderedDataObject) {
143
+ return {
144
+ ...createDataObject(rawRenderedDataObject),
145
+ ...structuredClone(renderedDataObjectDefaults),
146
+ ...structuredClone(rawRenderedDataObject)
147
+ };
148
+ }
149
+ /**
150
+ * Default values for {@link RawRenderedRasterDataObject}
151
+ */
152
+ var renderedRasterDataObjectDefaults = {};
153
+ /**
154
+ * Creates a {@link RenderedRasterDataObject} from a {@link RawRenderedRasterDataObject} by applying {@link renderedRasterDataObjectDefaults}
155
+ *
156
+ * @param rawRenderedRasterDataObject - The raw rendered raster data object
157
+ * @returns The complete rendered raster data object with default values applied
158
+ */
159
+ function createRenderedRasterDataObject(rawRenderedRasterDataObject) {
160
+ return {
161
+ ...createRenderedDataObject(rawRenderedRasterDataObject),
162
+ ...structuredClone(renderedRasterDataObjectDefaults),
163
+ ...structuredClone(rawRenderedRasterDataObject)
164
+ };
165
+ }
166
+ /**
167
+ * Default values for {@link RawRenderedItemsDataObject}
168
+ */
169
+ var renderedItemsDataObjectDefaults = {};
170
+ /**
171
+ * Creates a {@link RenderedItemsDataObject} from a {@link RawRenderedItemsDataObject} by applying {@link renderedItemsDataObjectDefaults}
172
+ *
173
+ * @param rawRenderedItemsDataObject - The raw rendered items data object
174
+ * @returns The complete rendered items data object with default values applied
175
+ */
176
+ function createRenderedItemsDataObject(rawRenderedItemsDataObject) {
177
+ return {
178
+ ...createRenderedDataObject(rawRenderedItemsDataObject),
179
+ ...structuredClone(renderedItemsDataObjectDefaults),
180
+ ...structuredClone(rawRenderedItemsDataObject)
181
+ };
182
+ }
183
+ /**
184
+ * Default values for {@link RawDataSource}
185
+ */
186
+ var dataSourceDefaults = {};
187
+ /**
188
+ * Creates a {@link DataSource} from a {@link RawDataSource} by applying {@link dataSourceDefaults}
189
+ *
190
+ * @param rawDataSource - The raw data source
191
+ * @returns The complete data source with default values applied
192
+ */
193
+ function createDataSource(rawDataSource) {
194
+ return {
195
+ ...createModel(rawDataSource),
196
+ ...structuredClone(dataSourceDefaults),
197
+ ...structuredClone(rawDataSource)
198
+ };
199
+ }
200
+ /**
201
+ * Default values for {@link RawAnnotatedDataSource}
202
+ */
203
+ var annotatedDataSourceDefaults = {};
204
+ /**
205
+ * Creates an {@link AnnotatedDataSource} from a {@link RawAnnotatedDataSource} by applying {@link annotatedDataSourceDefaults}
206
+ *
207
+ * @param rawAnnotatedDataSource - The raw annotated data source
208
+ * @returns The complete annotated data source with default values applied
209
+ */
210
+ function createAnnotatedDataSource(rawAnnotatedDataSource) {
211
+ return {
212
+ ...createDataSource(rawAnnotatedDataSource),
213
+ ...structuredClone(annotatedDataSourceDefaults),
214
+ ...structuredClone(rawAnnotatedDataSource)
215
+ };
216
+ }
217
+ //#endregion
218
+ //#region src/model/configs.ts
219
+ /**
220
+ * Determines the active source of the given {@link Config}
221
+ *
222
+ * If a source is explicitly prioritized using {@link Config.source}, that source is returned.
223
+ *
224
+ * Otherwise, the active source is determined by checking for the presence of configuration-specific fields in the following order:
225
+ * - {@link ConstantConfig}
226
+ * - {@link FromConfig}
227
+ * - {@link GroupByConfig}
228
+ * - {@link RandomConfig}
229
+ *
230
+ * @param config - The configuration
231
+ * @returns The active configuration source, or `undefined` if none is active
232
+ */
233
+ function getActiveConfigSource(config) {
234
+ if (config.source !== void 0) return config.source;
235
+ if (isConstantConfig(config)) return "constant";
236
+ if (isFromConfig(config)) return "from";
237
+ if (isGroupByConfig(config)) return "groupBy";
238
+ if (isRandomConfig(config)) return "random";
239
+ }
240
+ /**
241
+ * Determines whether the given object is a {@link ConstantConfig}
242
+ *
243
+ * @param obj - The object to check
244
+ * @returns Whether the object is a {@link ConstantConfig}
245
+ */
246
+ function isConstantConfig(obj) {
247
+ return obj.constant !== void 0;
248
+ }
249
+ /**
250
+ * Determines whether the given object is a {@link FromConfig}
251
+ *
252
+ * @param obj - The object to check
253
+ * @returns Whether the object is a {@link FromConfig}
254
+ */
255
+ function isFromConfig(obj) {
256
+ return obj.from !== void 0;
257
+ }
258
+ /**
259
+ * Determines whether the given object is a {@link GroupByConfig}
260
+ *
261
+ * @param obj - The object to check
262
+ * @returns Whether the object is a {@link GroupByConfig}
263
+ */
264
+ function isGroupByConfig(obj) {
265
+ return obj.groupBy !== void 0;
266
+ }
267
+ /**
268
+ * Determines whether the given object is a {@link RandomConfig}
269
+ *
270
+ * @param obj - The object to check
271
+ * @returns Whether the object is a {@link RandomConfig}
272
+ */
273
+ function isRandomConfig(obj) {
274
+ return obj.random !== void 0;
275
+ }
276
+ //#endregion
277
+ //#region src/model/image.ts
278
+ /**
279
+ * How the channels of multi-channel image data are displayed
280
+ *
281
+ * - `composite`: all visible channels, blended in their colors
282
+ * - `grayscale`: the active channel alone, in gray
283
+ * - `color`: the active channel alone, in its color
284
+ */
285
+ var ImageChannelViewMode = {
286
+ composite: "composite",
287
+ grayscale: "grayscale",
288
+ color: "color"
289
+ };
290
+ /**
291
+ * Default values for {@link RawImage}
292
+ */
293
+ var imageDefaults = {
294
+ channelViewMode: ImageChannelViewMode.composite,
295
+ activeChannel: 0
296
+ };
297
+ /**
298
+ * Creates an {@link Image} from a {@link RawImage} by applying {@link imageDefaults}
299
+ *
300
+ * @param rawImage - The raw image
301
+ * @returns The complete image with default values applied
302
+ */
303
+ function createImage(rawImage) {
304
+ return {
305
+ ...createRenderedRasterDataObject(rawImage),
306
+ ...structuredClone(imageDefaults),
307
+ ...structuredClone(rawImage),
308
+ dataSource: createImageDataSource(rawImage.dataSource)
309
+ };
310
+ }
311
+ /**
312
+ * Default values for {@link RawImageDataSource}
313
+ */
314
+ var imageDataSourceDefaults = {};
315
+ /**
316
+ * Creates an {@link ImageDataSource} from a {@link RawImageDataSource} by applying {@link imageDataSourceDefaults}
317
+ *
318
+ * @param rawImageDataSource - The raw image data source
319
+ * @returns The complete image data source with default values applied
320
+ */
321
+ function createImageDataSource(rawImageDataSource) {
322
+ return {
323
+ ...createDataSource(rawImageDataSource),
324
+ ...structuredClone(imageDataSourceDefaults),
325
+ ...structuredClone(rawImageDataSource)
326
+ };
327
+ }
328
+ //#endregion
329
+ //#region src/model/labels.ts
330
+ /**
331
+ * Default values for {@link RawLabels}
332
+ */
333
+ var labelsDefaults = {
334
+ labelColor: { random: { palette: defaultLabelColorPalette } },
335
+ labelVisibility: { constant: { value: true } },
336
+ labelOpacity: { constant: { value: 1 } }
337
+ };
338
+ /**
339
+ * Creates a {@link Labels} from a {@link RawLabels} by applying {@link labelsDefaults}
340
+ *
341
+ * @param rawLabels - The raw labels
342
+ * @returns The complete labels with default values applied
343
+ */
344
+ function createLabels(rawLabels) {
345
+ return {
346
+ ...createRenderedRasterDataObject(rawLabels),
347
+ ...structuredClone(labelsDefaults),
348
+ ...structuredClone(rawLabels),
349
+ dataSource: createLabelsDataSource(rawLabels.dataSource)
350
+ };
351
+ }
352
+ /**
353
+ * Default values for {@link RawLabelsDataSource}
354
+ */
355
+ var labelsDataSourceDefaults = {};
356
+ /**
357
+ * Creates a {@link LabelsDataSource} from a {@link RawLabelsDataSource} by applying {@link labelsDataSourceDefaults}
358
+ *
359
+ * @param rawLabelsDataSource - The raw labels data source
360
+ * @returns The complete labels data source with default values applied
361
+ */
362
+ function createLabelsDataSource(rawLabelsDataSource) {
363
+ return {
364
+ ...createAnnotatedDataSource(rawLabelsDataSource),
365
+ ...structuredClone(labelsDataSourceDefaults),
366
+ ...structuredClone(rawLabelsDataSource)
367
+ };
368
+ }
369
+ //#endregion
370
+ //#region src/model/layer.ts
371
+ /**
372
+ * Default values for {@link RawLayer}
373
+ */
374
+ var layerDefaults = {
375
+ transform: identityTransform,
376
+ visibility: true,
377
+ opacity: 1,
378
+ pointSizeFactor: 1
379
+ };
380
+ /**
381
+ * Creates a {@link Layer} from a {@link RawLayer} by applying {@link layerDefaults}
382
+ *
383
+ * @param rawLayer - The raw layer
384
+ * @returns The complete layer with default values applied
385
+ */
386
+ function createLayer(rawLayer) {
387
+ return {
388
+ ...createModel(rawLayer),
389
+ ...structuredClone(layerDefaults),
390
+ ...structuredClone(rawLayer)
391
+ };
392
+ }
393
+ //#endregion
394
+ //#region src/model/points.ts
395
+ /**
396
+ * Default values for {@link RawPoints}
397
+ */
398
+ var pointsDefaults = {
399
+ pointMarker: { constant: { value: defaultPointMarker } },
400
+ pointSize: { constant: { value: 1 } },
401
+ pointColor: { constant: { value: defaultPointColor } },
402
+ pointVisibility: { constant: { value: true } },
403
+ pointOpacity: { constant: { value: 1 } },
404
+ pointSizeFactor: 1
405
+ };
406
+ /**
407
+ * Creates a {@link Points} from a {@link RawPoints} by applying {@link pointsDefaults}
408
+ *
409
+ * @param rawPoints - The raw points
410
+ * @returns The complete points with default values applied
411
+ */
412
+ function createPoints(rawPoints) {
413
+ return {
414
+ ...createRenderedItemsDataObject(rawPoints),
415
+ ...structuredClone(pointsDefaults),
416
+ ...structuredClone(rawPoints),
417
+ dataSource: createPointsDataSource(rawPoints.dataSource)
418
+ };
419
+ }
420
+ /**
421
+ * Default values for {@link RawPointsDataSource}
422
+ */
423
+ var pointsDataSourceDefaults = {};
424
+ /**
425
+ * Creates a {@link PointsDataSource} from a {@link RawPointsDataSource} by applying {@link pointsDataSourceDefaults}
426
+ *
427
+ * @param rawPointsDataSource - The raw points data source
428
+ * @returns The complete points data source with default values applied
429
+ */
430
+ function createPointsDataSource(rawPointsDataSource) {
431
+ return {
432
+ ...createAnnotatedDataSource(rawPointsDataSource),
433
+ ...structuredClone(pointsDataSourceDefaults),
434
+ ...structuredClone(rawPointsDataSource)
435
+ };
436
+ }
437
+ //#endregion
438
+ //#region src/model/shapes.ts
439
+ /**
440
+ * Default values for {@link RawShapes}
441
+ */
442
+ var shapesDefaults = {
443
+ shapeVisibility: { constant: { value: true } },
444
+ shapeOpacity: { constant: { value: 1 } },
445
+ shapeFillColor: { constant: { value: defaultShapeFillColor } },
446
+ shapeFillVisibility: { constant: { value: true } },
447
+ shapeFillOpacity: { constant: { value: 1 } },
448
+ shapeStrokeColor: { constant: { value: defaultShapeStrokeColor } },
449
+ shapeStrokeVisibility: { constant: { value: true } },
450
+ shapeStrokeOpacity: { constant: { value: 1 } }
451
+ };
452
+ /**
453
+ * Creates a {@link Shapes} from a {@link RawShapes} by applying {@link shapesDefaults}
454
+ *
455
+ * @param rawShapes - The raw shapes
456
+ * @returns The complete shapes with default values applied
457
+ */
458
+ function createShapes(rawShapes) {
459
+ return {
460
+ ...createRenderedItemsDataObject(rawShapes),
461
+ ...structuredClone(shapesDefaults),
462
+ ...structuredClone(rawShapes),
463
+ dataSource: createShapesDataSource(rawShapes.dataSource)
464
+ };
465
+ }
466
+ /**
467
+ * Default values for {@link RawShapesDataSource}
468
+ */
469
+ var shapesDataSourceDefaults = {};
470
+ /**
471
+ * Creates a {@link ShapesDataSource} from a {@link RawShapesDataSource} by applying {@link shapesDataSourceDefaults}
472
+ *
473
+ * @param rawShapesDataSource - The raw shapes data source
474
+ * @returns The complete shapes data source with default values applied
475
+ */
476
+ function createShapesDataSource(rawShapesDataSource) {
477
+ return {
478
+ ...createAnnotatedDataSource(rawShapesDataSource),
479
+ ...structuredClone(shapesDataSourceDefaults),
480
+ ...structuredClone(rawShapesDataSource)
481
+ };
482
+ }
483
+ //#endregion
484
+ //#region src/model/table.ts
485
+ /**
486
+ * Default values for {@link RawTable}
487
+ */
488
+ var tableDefaults = {};
489
+ /**
490
+ * Creates a {@link Table} from a {@link RawTable} by applying {@link tableDefaults}
491
+ *
492
+ * @param rawTable - The raw table
493
+ * @returns The complete table with default values applied
494
+ */
495
+ function createTable(rawTable) {
496
+ return {
497
+ ...createDataObject(rawTable),
498
+ ...structuredClone(tableDefaults),
499
+ ...structuredClone(rawTable),
500
+ dataSource: createTableDataSource(rawTable.dataSource)
501
+ };
502
+ }
503
+ /**
504
+ * Default values for {@link RawTableDataSource}
505
+ */
506
+ var tableDataSourceDefaults = {};
507
+ /**
508
+ * Creates a {@link TableDataSource} from a {@link RawTableDataSource} by applying {@link tableDataSourceDefaults}
509
+ *
510
+ * @param rawTableDataSource - The raw table data source
511
+ * @returns The complete table data source with default values applied
512
+ */
513
+ function createTableDataSource(rawTableDataSource) {
514
+ return {
515
+ ...createDataSource(rawTableDataSource),
516
+ ...structuredClone(tableDataSourceDefaults),
517
+ ...structuredClone(rawTableDataSource)
518
+ };
519
+ }
520
+ //#endregion
521
+ //#region src/model/project.ts
522
+ /**
523
+ * Default values for {@link RawProject}
524
+ */
525
+ var projectDefaults = {
526
+ markerMaps: [],
527
+ sizeMaps: [],
528
+ colorMaps: [],
529
+ visibilityMaps: [],
530
+ opacityMaps: [],
531
+ osOptions: {
532
+ viewerOptions: {
533
+ minZoomImageRatio: 0,
534
+ maxZoomPixelRatio: Infinity,
535
+ preserveImageSizeOnResize: true,
536
+ visibilityRatio: 0,
537
+ animationTime: 0,
538
+ gestureSettingsMouse: { flickEnabled: false },
539
+ gestureSettingsTouch: { flickEnabled: false },
540
+ gestureSettingsPen: { flickEnabled: false },
541
+ gestureSettingsUnknown: { flickEnabled: false },
542
+ zoomPerClick: 1,
543
+ showNavigator: true,
544
+ navigatorPosition: "BOTTOM_LEFT",
545
+ maxImageCacheCount: 2e3,
546
+ showNavigationControl: false,
547
+ imageSmoothingEnabled: false
548
+ },
549
+ viewerAnimationStartOptions: {
550
+ immediateRender: false,
551
+ imageLoaderLimit: 1
552
+ },
553
+ viewerAnimationFinishOptions: { immediateRender: true }
554
+ },
555
+ glOptions: {
556
+ pointsRenderOptions: { globalPointSizeFactor: 1 },
557
+ shapesRenderOptions: {
558
+ strokeWidth: 1,
559
+ edgesPerScanline: 8,
560
+ binWidthFactor: 1,
561
+ shapePadding: .2
562
+ }
563
+ },
564
+ viewerBackgroundColor: {
565
+ r: 0,
566
+ g: 0,
567
+ b: 0
568
+ }
569
+ };
570
+ /**
571
+ * Creates a {@link Project} from a {@link RawProject} by applying {@link projectDefaults}
572
+ *
573
+ * @param rawProject - The raw project
574
+ * @returns The complete project with default values applied
575
+ */
576
+ function createProject(rawProject) {
577
+ return {
578
+ ...createModel(rawProject),
579
+ ...structuredClone(projectDefaults),
580
+ ...structuredClone(rawProject),
581
+ osOptions: {
582
+ viewerOptions: {
583
+ ...projectDefaults.osOptions.viewerOptions,
584
+ ...rawProject.osOptions?.viewerOptions
585
+ },
586
+ viewerAnimationStartOptions: {
587
+ ...projectDefaults.osOptions.viewerAnimationStartOptions,
588
+ ...rawProject.osOptions?.viewerAnimationStartOptions
589
+ },
590
+ viewerAnimationFinishOptions: {
591
+ ...projectDefaults.osOptions.viewerAnimationFinishOptions,
592
+ ...rawProject.osOptions?.viewerAnimationFinishOptions
593
+ }
594
+ },
595
+ glOptions: {
596
+ pointsRenderOptions: {
597
+ ...projectDefaults.glOptions.pointsRenderOptions,
598
+ ...rawProject.glOptions?.pointsRenderOptions
599
+ },
600
+ shapesRenderOptions: {
601
+ ...projectDefaults.glOptions.shapesRenderOptions,
602
+ ...rawProject.glOptions?.shapesRenderOptions
603
+ }
604
+ },
605
+ layers: rawProject.layers?.map(createLayer) ?? [],
606
+ images: rawProject.images?.map(createImage) ?? [],
607
+ labels: rawProject.labels?.map(createLabels) ?? [],
608
+ points: rawProject.points?.map(createPoints) ?? [],
609
+ shapes: rawProject.shapes?.map(createShapes) ?? [],
610
+ tables: rawProject.tables?.map(createTable) ?? []
611
+ };
612
+ }
613
+ //#endregion
614
+ //#region src/utils/NumberUtils.ts
615
+ /**
616
+ * Utility methods for coercing untrusted values to numbers
617
+ *
618
+ * Numbers, bigints and non-blank strings are accepted; everything else, and
619
+ * everything that does not coerce to a finite number, is rejected. The
620
+ * `tryParse*` methods report rejection by returning `undefined`, the `parse*`
621
+ * methods by throwing.
622
+ */
623
+ var NumberUtils = class NumberUtils {
624
+ static _minSafeBigInt = BigInt(Number.MIN_SAFE_INTEGER);
625
+ static _maxSafeBigInt = BigInt(Number.MAX_SAFE_INTEGER);
626
+ /**
627
+ * Parses a value as a finite number
628
+ *
629
+ * @param value - The value to parse
630
+ * @returns The parsed number
631
+ * @throws Error if the value is not a finite number
632
+ */
633
+ static parseFinite(value) {
634
+ const v = NumberUtils.tryParseFinite(value);
635
+ if (v === void 0) throw new Error(`Value is not a finite number: ${String(value)}`);
636
+ return v;
637
+ }
638
+ /**
639
+ * Parses a value as a safe integer
640
+ *
641
+ * @param value - The value to parse
642
+ * @returns The parsed integer
643
+ * @throws Error if the value is not a safe integer
644
+ */
645
+ static parseSafeInt(value) {
646
+ const v = NumberUtils.tryParseSafeInt(value);
647
+ if (v === void 0) throw new Error(`Value is not a safe integer: ${String(value)}`);
648
+ return v;
649
+ }
650
+ /**
651
+ * Parses a value as a finite number, without throwing
652
+ *
653
+ * A bigint outside of the safe integer range loses precision when converted:
654
+ * by default this is only warned about, `requireSafeBigInt` rejects it.
655
+ *
656
+ * @param value - The value to parse
657
+ * @param options - Set `requireSafeBigInt` to reject bigints that are outside
658
+ * of the safe integer range, rather than converting them with a warning
659
+ * @returns The parsed number, or `undefined` if the value is not a finite
660
+ * number
661
+ */
662
+ static tryParseFinite(value, options) {
663
+ let v;
664
+ if (typeof value === "number") v = value;
665
+ else if (typeof value === "bigint") {
666
+ if (value < NumberUtils._minSafeBigInt || value > NumberUtils._maxSafeBigInt) {
667
+ if (options?.requireSafeBigInt) return;
668
+ console.warn(`Value ${value} is outside the safe integer range`);
669
+ }
670
+ v = Number(value);
671
+ } else if (typeof value === "string" && value.trim() !== "") v = Number(value);
672
+ else return;
673
+ return Number.isFinite(v) ? v : void 0;
674
+ }
675
+ /**
676
+ * Parses a value as a safe integer, without throwing
677
+ *
678
+ * @param value - The value to parse
679
+ * @returns The parsed integer, or `undefined` if the value is not a safe
680
+ * integer
681
+ */
682
+ static tryParseSafeInt(value) {
683
+ const v = NumberUtils.tryParseFinite(value, { requireSafeBigInt: true });
684
+ if (v === void 0) return;
685
+ return Number.isSafeInteger(v) ? v : void 0;
686
+ }
687
+ };
688
+ //#endregion
689
+ //#region src/utils/ArrayUtils.ts
690
+ /**
691
+ * Utility methods for converting arrays to the types the storage API holds
692
+ */
693
+ var ArrayUtils = class ArrayUtils {
694
+ /**
695
+ * Converts item IDs to an `IDArray`
696
+ *
697
+ * Integer typed arrays, string arrays and 64-bit float arrays holding safe
698
+ * integers are returned as they are. Any other array has to hold either
699
+ * strings only, which are copied into a string array, or safe integers only,
700
+ * which are copied into 64-bit floats. Numeric strings are not parsed:
701
+ * whether `"12"` is an ID or a number is up to the data provider. An empty
702
+ * array yields an empty unsigned 32-bit integer array.
703
+ *
704
+ * @param values - The IDs to convert
705
+ * @returns The IDs as an integer typed array, a 64-bit float array or a
706
+ * string array
707
+ * @throws Error if a value is neither a string nor a safe integer, or if
708
+ * strings and integers are mixed
709
+ */
710
+ static toIDArray(values) {
711
+ if (ArrayUtils._isIntOrUintArray(values)) return values;
712
+ if (values.length === 0) return /* @__PURE__ */ new Uint32Array(0);
713
+ if (typeof values[0] === "string") {
714
+ const ids = Array.isArray(values) ? values : Array.from(values);
715
+ for (const id of ids) if (typeof id !== "string") throw new Error(`ID is not a string: ${String(id)}`);
716
+ return ids;
717
+ }
718
+ const ids = values instanceof Float64Array ? values : new Float64Array(values.length);
719
+ for (let i = 0; i < values.length; i++) {
720
+ const id = values[i];
721
+ if (typeof id !== "number" || !Number.isSafeInteger(id)) throw new Error(`ID is not a safe integer: ${String(id)}`);
722
+ ids[i] = id;
723
+ }
724
+ return ids;
725
+ }
726
+ /**
727
+ * Tells whether an array is an integer typed array of at most 32 bits
728
+ */
729
+ static _isIntOrUintArray(values) {
730
+ return values instanceof Int8Array || values instanceof Int16Array || values instanceof Int32Array || values instanceof Uint8Array || values instanceof Uint8ClampedArray || values instanceof Uint16Array || values instanceof Uint32Array;
731
+ }
732
+ /**
733
+ * Converts 64-bit integers to 64-bit floats
734
+ *
735
+ * @param values - The integers, as read from a file (e.g. a `BigInt64Array`)
736
+ * @returns The integers as floats
737
+ * @throws Error if an integer is outside the safe integer range
738
+ */
739
+ static parseSafeInts(values) {
740
+ return Float64Array.from(values, (v) => NumberUtils.parseSafeInt(v));
741
+ }
742
+ };
743
+ //#endregion
744
+ //#region src/utils/AsyncUtils.ts
745
+ /**
746
+ * Utility methods for running large CPU workloads on the main thread without
747
+ * blocking the UI, by cooperatively yielding to the event loop
748
+ */
749
+ var AsyncUtils = class AsyncUtils {
750
+ static _messageChannel;
751
+ static _pendingResolvers = [];
752
+ /**
753
+ * Yields control back to the event loop, allowing the browser to paint and
754
+ * process input before resuming.
755
+ *
756
+ * Uses the native `scheduler.yield()` when available, otherwise falls back to
757
+ * a shared `MessageChannel` (a macrotask without `setTimeout`'s clamping).
758
+ *
759
+ * @returns A promise that resolves once the event loop has been yielded to
760
+ */
761
+ static yield() {
762
+ const scheduler = globalThis.scheduler;
763
+ if (typeof scheduler?.yield === "function") return scheduler.yield();
764
+ if (AsyncUtils._messageChannel === void 0) {
765
+ AsyncUtils._messageChannel = new MessageChannel();
766
+ AsyncUtils._messageChannel.port1.onmessage = () => {
767
+ AsyncUtils._pendingResolvers.shift()?.();
768
+ };
769
+ }
770
+ return new Promise((resolve) => {
771
+ AsyncUtils._pendingResolvers.push(resolve);
772
+ AsyncUtils._messageChannel.port2.postMessage(void 0);
773
+ });
774
+ }
775
+ /**
776
+ * Creates a time-sliced yielder for loops whose iterations are too coarse or
777
+ * heterogeneous for {@link AsyncUtils.forEach} (e.g. nested loops).
778
+ *
779
+ * Call the returned function at safe points inside a loop: it checks the
780
+ * abort signal on every call for fail-fast cancellation, and additionally
781
+ * yields to the event loop once the time budget since the last yield is
782
+ * exceeded. When within budget and not aborted, it is a cheap no-op.
783
+ *
784
+ * @param options - Optional time (ms) to run before yielding
785
+ * @returns The yielder, which takes an optional abort signal
786
+ */
787
+ static createYielder(options) {
788
+ const { yieldMs = 5 } = options ?? {};
789
+ let start = performance.now();
790
+ return async (opts) => {
791
+ const { signal } = opts ?? {};
792
+ signal?.throwIfAborted();
793
+ if (performance.now() - start > yieldMs) {
794
+ await AsyncUtils.yield();
795
+ signal?.throwIfAborted();
796
+ start = performance.now();
797
+ }
798
+ };
799
+ }
800
+ /**
801
+ * Invokes `callback(item, index)` for every element of `items`, yielding to
802
+ * the event loop whenever the time budget is exceeded so the UI stays
803
+ * responsive.
804
+ *
805
+ * The abort signal is checked up front and after every yield, preserving
806
+ * fail-fast cancellation: an abort rejects with the signal's reason and no
807
+ * further iterations run.
808
+ *
809
+ * @param items - The array-like collection to iterate over
810
+ * @param callback - Synchronous work to perform for each `item` and its `index`
811
+ * @param options - Optional abort signal, time (ms) to run before yielding
812
+ * (`yieldMs`), and the number of iterations between budget checks
813
+ * (`checkEvery`)
814
+ * @returns A promise that resolves once every item has been visited, or
815
+ * rejects with the signal's reason
816
+ */
817
+ static async forEach(items, callback, options) {
818
+ const { signal, yieldMs, checkEvery = 1024 } = options ?? {};
819
+ signal?.throwIfAborted();
820
+ const maybeYield = AsyncUtils.createYielder({ yieldMs });
821
+ for (let i = 0; i < items.length; i++) {
822
+ callback(items[i], i);
823
+ if ((i + 1) % checkEvery === 0 && i + 1 < items.length) await maybeYield({ signal });
824
+ }
825
+ }
826
+ /**
827
+ * Returns a promise that resolves or rejects with the given `promise`, but
828
+ * rejects early if the given `signal` is aborted.
829
+ *
830
+ * @param promise - The promise to race against the abort signal
831
+ * @param options - Optional abort signal to race against the promise
832
+ * @returns A promise that resolves or rejects based on the race condition
833
+ */
834
+ static async raceSignal(promise, options) {
835
+ const { signal } = options ?? {};
836
+ if (signal === void 0) return promise;
837
+ signal.throwIfAborted();
838
+ return new Promise((resolve, reject) => {
839
+ const onAbort = () => reject(signal.reason);
840
+ signal.addEventListener("abort", onAbort, { once: true });
841
+ promise.then((value) => {
842
+ signal.removeEventListener("abort", onAbort);
843
+ resolve(value);
844
+ }, (error) => {
845
+ signal.removeEventListener("abort", onAbort);
846
+ reject(error);
847
+ });
848
+ });
849
+ }
850
+ /**
851
+ * Creates a promise together with the functions that resolve and reject it,
852
+ * like `Promise.withResolvers()`, which is not yet widely available.
853
+ *
854
+ * @returns The promise and the functions that resolve and reject it
855
+ */
856
+ static withResolvers() {
857
+ let resolve;
858
+ let reject;
859
+ return {
860
+ promise: new Promise((resolvePromise, rejectPromise) => {
861
+ resolve = resolvePromise;
862
+ reject = rejectPromise;
863
+ }),
864
+ resolve,
865
+ reject
866
+ };
867
+ }
868
+ };
869
+ //#endregion
870
+ //#region src/utils/BitUtils.ts
871
+ /** Utility methods for bitwise operations that yield unsigned 32-bit results */
872
+ var BitUtils = class {
873
+ /**
874
+ * Performs a bitwise AND, returning an unsigned 32-bit result
875
+ *
876
+ * @param a - First operand
877
+ * @param b - Second operand
878
+ * @returns The result, as an unsigned 32-bit integer
879
+ */
880
+ static safeAnd(a, b) {
881
+ return (a & b) >>> 0;
882
+ }
883
+ /**
884
+ * Performs a bitwise OR, returning an unsigned 32-bit result
885
+ *
886
+ * @param a - First operand
887
+ * @param b - Second operand
888
+ * @returns The result, as an unsigned 32-bit integer
889
+ */
890
+ static safeOr(a, b) {
891
+ return (a | b) >>> 0;
892
+ }
893
+ /**
894
+ * Performs a bitwise XOR, returning an unsigned 32-bit result
895
+ *
896
+ * @param a - First operand
897
+ * @param b - Second operand
898
+ * @returns The result, as an unsigned 32-bit integer
899
+ */
900
+ static safeXor(a, b) {
901
+ return (a ^ b) >>> 0;
902
+ }
903
+ /**
904
+ * Performs a bitwise NOT, returning an unsigned 32-bit result
905
+ *
906
+ * @param a - The operand
907
+ * @returns The result, as an unsigned 32-bit integer
908
+ */
909
+ static safeNot(a) {
910
+ return ~a >>> 0;
911
+ }
912
+ /**
913
+ * Performs a left shift, returning an unsigned 32-bit result
914
+ *
915
+ * @param value - The value to shift
916
+ * @param shift - Number of bit positions to shift
917
+ * @returns The result, as an unsigned 32-bit integer
918
+ */
919
+ static safeLeftShift(value, shift) {
920
+ return value << shift >>> 0;
921
+ }
922
+ /**
923
+ * Performs an unsigned right shift
924
+ *
925
+ * @param value - The value to shift
926
+ * @param shift - Number of bit positions to shift
927
+ * @returns The result, as an unsigned 32-bit integer
928
+ */
929
+ static safeRightShift(value, shift) {
930
+ return value >>> shift;
931
+ }
932
+ };
933
+ //#endregion
934
+ //#region src/utils/HashUtils.ts
935
+ /**
936
+ * Utility methods for non-cryptographic hashing
937
+ *
938
+ * Hashing a string is split into two steps: {@link hashRaw} reduces the
939
+ * string to a 32-bit integer, and {@link mix} scrambles that integer together
940
+ * with a seed. Callers that hash the same key repeatedly with different seeds
941
+ * can cache the raw hash and only re-run the cheap mixing step. {@link hash}
942
+ * does both in one call.
943
+ *
944
+ * The underlying primitives, {@link fnv1a} and {@link fmix32}, are exposed
945
+ * for callers that need the plain algorithms without the seeding convention
946
+ * of the methods above.
947
+ */
948
+ var HashUtils = class HashUtils {
949
+ static _murmur3Config = {
950
+ s1: 16,
951
+ m1: 2246822507,
952
+ s2: 13,
953
+ m2: 3266489909,
954
+ s3: 16
955
+ };
956
+ /**
957
+ * Computes the raw hash of a string
958
+ *
959
+ * Currently {@link fnv1a}. The raw hash is fast and deterministic, but only
960
+ * weakly mixed: similar keys yield correlated hashes, and the hash of an
961
+ * empty string is non-zero. Pass the result through {@link mix} before
962
+ * using it to select from a small range.
963
+ *
964
+ * @param key - The string to hash; hashed by UTF-16 code unit
965
+ * @returns The raw hash, as a non-negative 32-bit integer
966
+ */
967
+ static hashRaw(key) {
968
+ return HashUtils.fnv1a(key);
969
+ }
970
+ /**
971
+ * Scrambles a 32-bit integer with a seed
972
+ *
973
+ * The seed is XOR-ed into the value, which is then passed through
974
+ * {@link fmix32}. Consecutive inputs map to unrelated outputs, so the result
975
+ * is suitable for scattering sequential IDs or raw string hashes over a
976
+ * small range, e.g. `mix(id, seed) % palette.length`. Different seeds yield
977
+ * different outputs for the same input.
978
+ *
979
+ * @param h - The integer to mix; only its lower 32 bits are used
980
+ * @param seed - The seed; only its lower 32 bits are used
981
+ * @returns The mixed value, as a non-negative 32-bit integer
982
+ */
983
+ static mix(h, seed = 0) {
984
+ return HashUtils.fmix32(h ^ seed);
985
+ }
986
+ /**
987
+ * Computes a seeded 32-bit hash of a string
988
+ *
989
+ * Equivalent to `mix(hashRaw(key), seed)`.
990
+ *
991
+ * @param key - The string to hash; hashed by UTF-16 code unit
992
+ * @param seed - The seed; only its lower 32 bits are used
993
+ * @returns The hash, as a non-negative 32-bit integer
994
+ */
995
+ static hash(key, seed = 0) {
996
+ return HashUtils.mix(HashUtils.hashRaw(key), seed);
997
+ }
998
+ /**
999
+ * Computes the 32-bit FNV-1a hash of a string
1000
+ *
1001
+ * Iterates over the UTF-16 code units of the string, XOR-ing each into the
1002
+ * running hash and multiplying by the FNV prime. The hash of an empty string
1003
+ * is the FNV offset basis (`0x811c9dc5`). Code units above `0xff` are XOR-ed
1004
+ * in whole rather than byte-wise, so results for non-Latin-1 text differ
1005
+ * from byte-oriented FNV-1a implementations.
1006
+ *
1007
+ * @see http://www.isthe.com/chongo/tech/comp/fnv/
1008
+ * @param str - The string to hash
1009
+ * @returns The hash, as a non-negative 32-bit integer
1010
+ */
1011
+ static fnv1a(str) {
1012
+ let h = 2166136261;
1013
+ for (let i = 0; i < str.length; i++) h = Math.imul(h ^ str.charCodeAt(i), 16777619);
1014
+ return h >>> 0;
1015
+ }
1016
+ /**
1017
+ * Applies a three-round xorshift-multiply finalizer; defaults to MurmurHash3's constants
1018
+ *
1019
+ * The finalizer is a bijective avalanche step: every input bit affects
1020
+ * every output bit, and distinct 32-bit inputs map to distinct outputs.
1021
+ * Note that `fmix32(0)` is `0`.
1022
+ *
1023
+ * @see https://github.com/aappleby/smhasher/blob/master/src/MurmurHash3.cpp
1024
+ * @param h - The integer to finalize; only its lower 32 bits are used
1025
+ * @param c - The constants to use; defaults are the MurmurHash3 finalizer constants
1026
+ * @returns The finalized value, as a non-negative 32-bit integer
1027
+ */
1028
+ static fmix32(h, c = HashUtils._murmur3Config) {
1029
+ h ^= h >>> c.s1;
1030
+ h = Math.imul(h, c.m1);
1031
+ h ^= h >>> c.s2;
1032
+ h = Math.imul(h, c.m2);
1033
+ h ^= h >>> c.s3;
1034
+ return h >>> 0;
1035
+ }
1036
+ };
1037
+ //#endregion
1038
+ //#region src/utils/RandomUtils.ts
1039
+ /**
1040
+ * Utility methods for seeded, non-cryptographic pseudo-random numbers
1041
+ *
1042
+ * All methods except {@link seed} are deterministic: the same seed always
1043
+ * yields the same sequence, so callers can store a seed instead of the values
1044
+ * it produced (e.g. for reproducible random colors). None of the generators
1045
+ * are suitable for security-sensitive purposes.
1046
+ *
1047
+ * {@link createUint32RNG} returns a generator that draws one 32-bit integer
1048
+ * per call; {@link toUnitFloat32} maps such a draw to `[0, 1)`. The scalar
1049
+ * `rand*` methods take the first draw of a fresh generator, and the
1050
+ * `rand*Array` methods write consecutive draws of a fresh generator into a
1051
+ * typed array. Because they all consume the same stream,
1052
+ * `randUint32Array(n, seed)` starts with `randUint32(seed)`, and a longer
1053
+ * array has the shorter one as a prefix.
1054
+ *
1055
+ * Every `rand*` method takes either a seed or a generator as `seedOrRng`. A
1056
+ * seed creates a fresh generator; passing a generator draws from it instead,
1057
+ * which allows continuing one stream across several calls.
1058
+ *
1059
+ * The underlying generator, {@link splitmix32}, is exposed for callers that
1060
+ * need the plain algorithm regardless of the current default.
1061
+ */
1062
+ var RandomUtils = class RandomUtils {
1063
+ static _splitmix32Config = {
1064
+ s1: 16,
1065
+ m1: 569420461,
1066
+ s2: 15,
1067
+ m2: 1935289751,
1068
+ s3: 15
1069
+ };
1070
+ /**
1071
+ * Draws a random seed from `Math.random()`
1072
+ *
1073
+ * This is the one non-deterministic method of this class; use it to obtain
1074
+ * a seed for the other methods when reproducibility is not required, e.g.
1075
+ * for a "shuffle" action. The result is stored rather than the values it
1076
+ * produces, so the draw can be reproduced later.
1077
+ *
1078
+ * @returns A uniformly distributed non-negative 32-bit integer
1079
+ */
1080
+ static seed() {
1081
+ return Math.random() * 4294967296 >>> 0;
1082
+ }
1083
+ /**
1084
+ * Creates a seeded generator of 32-bit integers
1085
+ *
1086
+ * Currently {@link splitmix32}. Each call to the returned function yields
1087
+ * the next value of the stream.
1088
+ *
1089
+ * @param seed - The seed; only its lower 32 bits are used
1090
+ * @returns A function that returns the next non-negative 32-bit integer
1091
+ */
1092
+ static createUint32RNG(seed = 0) {
1093
+ return RandomUtils.splitmix32(seed);
1094
+ }
1095
+ /**
1096
+ * Draws a 32-bit integer
1097
+ *
1098
+ * Given a seed, returns the first value of a fresh generator for it, i.e.
1099
+ * `createUint32RNG(seed)()`. Note that this is a pure function of the seed:
1100
+ * calling it repeatedly with the same seed returns the same value. Given a
1101
+ * generator, returns its next value instead.
1102
+ *
1103
+ * @param seedOrRng - The seed (only its lower 32 bits are used), or the
1104
+ * generator to draw from
1105
+ * @returns A non-negative 32-bit integer
1106
+ */
1107
+ static randUint32(seedOrRng = 0) {
1108
+ return RandomUtils._toRNG(seedOrRng)();
1109
+ }
1110
+ /**
1111
+ * Draws consecutive 32-bit integers into an array
1112
+ *
1113
+ * Given a seed, values come from a fresh generator for it, so the first
1114
+ * element equals `randUint32(seed)`. Given a generator, its next `n` values
1115
+ * are drawn instead, e.g. to continue one stream across several arrays. By
1116
+ * default, a new array of length `n` is allocated; pass `array` to fill the
1117
+ * first `n` elements of an existing array in place instead, leaving any
1118
+ * further elements untouched.
1119
+ *
1120
+ * @param n - The number of values to draw, a non-negative integer
1121
+ * @param seedOrRng - The seed (only its lower 32 bits are used), or the
1122
+ * generator to draw from
1123
+ * @param array - The array to fill; defaults to `new Uint32Array(n)`
1124
+ * @returns The filled array, i.e. `array` if given
1125
+ * @throws RangeError if `array` is given and holds fewer than `n` elements
1126
+ */
1127
+ static randUint32Array(n, seedOrRng = 0, array = new Uint32Array(n)) {
1128
+ if (n > array.length) throw new RangeError("array must hold at least n elements");
1129
+ const rng = RandomUtils._toRNG(seedOrRng);
1130
+ for (let i = 0; i < n; i++) array[i] = rng();
1131
+ return array;
1132
+ }
1133
+ /**
1134
+ * Maps a 32-bit integer to a float in `[0, 1)` that is exact in `float32`
1135
+ *
1136
+ * Uses the upper 24 bits of the integer, so the result is a multiple of
1137
+ * `2^-24` and survives storage in a `Float32Array` without rounding. The
1138
+ * largest possible result is `1 - 2^-24`; the lower 8 bits of the input are
1139
+ * discarded.
1140
+ *
1141
+ * @param uint32 - The integer to map; only its lower 32 bits are used
1142
+ * @returns A float in `[0, 1)`
1143
+ */
1144
+ static toUnitFloat32(uint32) {
1145
+ return (uint32 >>> 8) / 16777216;
1146
+ }
1147
+ /**
1148
+ * Draws a float in `[0, 1)` that is exact in `float32`
1149
+ *
1150
+ * Equivalent to `toUnitFloat32(randUint32(seedOrRng))`. Given a seed, this
1151
+ * is a pure function of it: calling it repeatedly with the same seed returns
1152
+ * the same value. Given a generator, its next value is drawn instead.
1153
+ *
1154
+ * @param seedOrRng - The seed (only its lower 32 bits are used), or the
1155
+ * generator to draw from
1156
+ * @returns A float in `[0, 1)`, exact in `float32`
1157
+ */
1158
+ static randUnitFloat32(seedOrRng = 0) {
1159
+ return RandomUtils.toUnitFloat32(RandomUtils.randUint32(seedOrRng));
1160
+ }
1161
+ /**
1162
+ * Draws consecutive floats in `[0, 1)` into an array
1163
+ *
1164
+ * Each element is `toUnitFloat32` of the corresponding element that
1165
+ * {@link randUint32Array} would produce for the same arguments, and is
1166
+ * stored in the `Float32Array` without rounding. Given a seed, values come
1167
+ * from a fresh generator for it, so the first element equals
1168
+ * `randUnitFloat32(seed)`. Given a generator, its next `n` values are drawn
1169
+ * instead, e.g. to continue one stream across several arrays. By default, a
1170
+ * new array of length `n` is allocated; pass `array` to fill the first `n`
1171
+ * elements of an existing array in place instead, leaving any further
1172
+ * elements untouched.
1173
+ *
1174
+ * @param n - The number of values to draw, a non-negative integer
1175
+ * @param seedOrRng - The seed (only its lower 32 bits are used), or the
1176
+ * generator to draw from
1177
+ * @param array - The array to fill; defaults to `new Float32Array(n)`
1178
+ * @returns The filled array, i.e. `array` if given
1179
+ * @throws RangeError if `array` is given and holds fewer than `n` elements
1180
+ */
1181
+ static randUnitFloat32Array(n, seedOrRng = 0, array = new Float32Array(n)) {
1182
+ if (n > array.length) throw new RangeError("array must hold at least n elements");
1183
+ const rng = RandomUtils._toRNG(seedOrRng);
1184
+ for (let i = 0; i < n; i++) array[i] = RandomUtils.toUnitFloat32(rng());
1185
+ return array;
1186
+ }
1187
+ /**
1188
+ * Creates a SplitMix32 generator
1189
+ *
1190
+ * The state is a 32-bit counter advanced by the golden-ratio constant
1191
+ * `0x9e3779b9` (a Weyl sequence); each output is the state passed through
1192
+ * {@link HashUtils.fmix32} with the SplitMix32 constants. Since the
1193
+ * finalizer is a bijection, the stream has a period of `2^32` and visits
1194
+ * every 32-bit value exactly once per period. All seeds walk the same
1195
+ * cycle; seeds that differ in their lower 32 bits start at different points.
1196
+ *
1197
+ * @see https://github.com/bryc/code/blob/master/jshash/PRNGs.md#splitmix32
1198
+ * @param seed - The seed; only its lower 32 bits are used
1199
+ * @returns A function that returns the next non-negative 32-bit integer
1200
+ */
1201
+ static splitmix32(seed = 0) {
1202
+ let h = seed >>> 0;
1203
+ return () => {
1204
+ h = h + 2654435769 >>> 0;
1205
+ return HashUtils.fmix32(h, RandomUtils._splitmix32Config);
1206
+ };
1207
+ }
1208
+ /**
1209
+ * Resolves a seed or generator to a generator
1210
+ *
1211
+ * @param seedOrRng - A seed, or an existing generator
1212
+ * @returns A fresh generator for the seed, or the given generator
1213
+ */
1214
+ static _toRNG(seedOrRng) {
1215
+ return typeof seedOrRng === "number" ? RandomUtils.createUint32RNG(seedOrRng) : seedOrRng;
1216
+ }
1217
+ };
1218
+ //#endregion
1219
+ //#region src/utils/MathUtils.ts
1220
+ /**
1221
+ * Utility methods for numeric clamping, remapping, alignment, rounding,
1222
+ * medians, histograms and counts
1223
+ */
1224
+ var MathUtils = class MathUtils {
1225
+ /**
1226
+ * Clamps a value to the range `[min, max]`
1227
+ *
1228
+ * @param value - The value to clamp
1229
+ * @param min - Lower bound
1230
+ * @param max - Upper bound
1231
+ * @returns The clamped value
1232
+ */
1233
+ static clamp(value, min, max) {
1234
+ return Math.min(Math.max(min, value), max);
1235
+ }
1236
+ /**
1237
+ * Linearly maps a value from one range to another
1238
+ *
1239
+ * Values outside `from` are extrapolated, not clamped; clamp at the call
1240
+ * site where the domain is known. Swapping `from` and `to` yields the
1241
+ * inverse mapping.
1242
+ *
1243
+ * @param value - The value to map
1244
+ * @param from - The source range, as `[min, max]`, with `min !== max`
1245
+ * @param to - The target range, as `[min, max]`
1246
+ * @returns The mapped value
1247
+ * @throws Error if `from` is degenerate
1248
+ */
1249
+ static remap(value, from, to) {
1250
+ const [fromMin, fromMax] = from;
1251
+ const [toMin, toMax] = to;
1252
+ if (fromMin === fromMax) throw new Error("from must not be degenerate");
1253
+ return toMin + (value - fromMin) / (fromMax - fromMin) * (toMax - toMin);
1254
+ }
1255
+ /**
1256
+ * Aligns a positive number `n` to the next multiple of `m`
1257
+ *
1258
+ * If `n` is already a multiple of `m`, returns `n`.
1259
+ * Otherwise, returns the smallest multiple of `m` that is greater than `n`.
1260
+ *
1261
+ * @param n - The non-negative number to align
1262
+ * @param m - The strictly positive multiple to align to
1263
+ * @returns The aligned number
1264
+ * @throws Error if `n` is negative, or if `m` is not strictly positive
1265
+ */
1266
+ static align(n, m) {
1267
+ if (n < 0) throw new Error("n must be non-negative");
1268
+ if (m <= 0) throw new Error("m must be strictly positive");
1269
+ if (n % m === 0) return n;
1270
+ return Math.ceil(n / m) * m;
1271
+ }
1272
+ /**
1273
+ * Rounds a value to the decimal places that a step needs
1274
+ *
1275
+ * Steps of `1` or more round to whole numbers, `0.1` to one decimal place,
1276
+ * `0.05` to two. Stepping through a range adds floating-point noise that this
1277
+ * rounds away, e.g. `0.30000000000000004` becomes `0.3` for a step of `0.1`.
1278
+ *
1279
+ * @param value - The value to round
1280
+ * @param step - The strictly positive step
1281
+ * @returns The rounded value
1282
+ * @throws Error if `step` is not strictly positive
1283
+ */
1284
+ static roundToStepDecimals(value, step) {
1285
+ if (step <= 0) throw new Error("step must be strictly positive");
1286
+ const decimals = Math.max(0, -Math.floor(Math.log10(step)));
1287
+ return Number(value.toFixed(decimals));
1288
+ }
1289
+ /**
1290
+ * Computes the weighted median of numeric values
1291
+ *
1292
+ * Returns the smallest value whose cumulative weight, in ascending order of
1293
+ * the values, reaches half the total weight. If the total weight is zero,
1294
+ * all values are weighted equally instead.
1295
+ *
1296
+ * @param values - The non-empty values to compute the weighted median of
1297
+ * @param weights - The non-negative weight of each value
1298
+ * @returns The weighted median
1299
+ * @throws Error if `values` is empty, or if `weights` has a different length
1300
+ */
1301
+ static computeWeightedMedian(values, weights) {
1302
+ if (values.length === 0) throw new Error("values must not be empty");
1303
+ if (weights.length !== values.length) throw new Error("weights must have the same length as values");
1304
+ const order = new Uint32Array(values.length).map((_, i) => i);
1305
+ order.sort((i, j) => values[i] - values[j]);
1306
+ let totalWeight = 0;
1307
+ for (let i = 0; i < weights.length; i++) totalWeight += weights[i];
1308
+ const weighted = totalWeight > 0;
1309
+ const halfWeight = .5 * (weighted ? totalWeight : values.length);
1310
+ let cumulativeWeight = 0;
1311
+ for (const i of order) {
1312
+ cumulativeWeight += weighted ? weights[i] : 1;
1313
+ if (cumulativeWeight >= halfWeight) return values[i];
1314
+ }
1315
+ return values[order[order.length - 1]];
1316
+ }
1317
+ /**
1318
+ * Computes the range of numeric values, as their minimum and maximum
1319
+ *
1320
+ * Non-finite values (`NaN`, infinities) are ignored. If no finite value is
1321
+ * found (e.g. for empty arrays), the empty range `[Infinity, -Infinity]` is
1322
+ * returned; callers can detect it by checking that the lower bound exceeds
1323
+ * the upper bound.
1324
+ *
1325
+ * The values are traversed on the main thread, yielding to the event loop
1326
+ * periodically so the UI stays responsive for large arrays (see
1327
+ * {@link AsyncUtils.forEach}). Aborting the signal rejects with its reason.
1328
+ *
1329
+ * @param values - The values to compute the range of
1330
+ * @param options - Optional abort signal
1331
+ * @returns A promise that resolves to the range, as `[min, max]`
1332
+ */
1333
+ static async computeRange(values, options) {
1334
+ const { signal } = options ?? {};
1335
+ signal?.throwIfAborted();
1336
+ let vmin = Infinity;
1337
+ let vmax = -Infinity;
1338
+ await AsyncUtils.forEach(values, (v) => {
1339
+ if (Number.isFinite(v)) {
1340
+ if (v < vmin) vmin = v;
1341
+ if (v > vmax) vmax = v;
1342
+ }
1343
+ }, { signal });
1344
+ return [vmin, vmax];
1345
+ }
1346
+ /**
1347
+ * Computes the histogram of numeric values over a given value range
1348
+ *
1349
+ * `hist[i]` counts the values that are closest to bin `i`, with bins spread
1350
+ * evenly over `range`: bin `0` maps to the range's lower bound, the last bin
1351
+ * to its upper bound, and each bin in between to `vmin + i / (bins - 1) *
1352
+ * (vmax - vmin)`. Values outside the range are counted in the nearest edge
1353
+ * bin, non-finite values (`NaN`, infinities) are ignored. If the range is
1354
+ * degenerate (upper bound not above lower bound), or if `bins` is `1`, all
1355
+ * values fall into bin `0`.
1356
+ *
1357
+ * If `sample` is positive and smaller than the number of values, the
1358
+ * histogram is estimated from `sample` values drawn uniformly with
1359
+ * replacement, using the seeded generator of
1360
+ * {@link RandomUtils.createUint32RNG}; the counts then sum to `sample`
1361
+ * (minus ignored non-finite values) rather than to the number of values.
1362
+ * Sampling is deterministic for a given `seed`. Otherwise, every value is
1363
+ * counted exactly once.
1364
+ *
1365
+ * The values are traversed on the main thread, yielding to the event loop
1366
+ * periodically so the UI stays responsive for large arrays (see
1367
+ * {@link AsyncUtils.forEach}). Aborting the signal rejects with its reason.
1368
+ *
1369
+ * @param values - The values to compute the histogram of
1370
+ * @param range - The value range the bins span, as `[min, max]`
1371
+ * @param options - Optional abort signal (`signal`), number of bins
1372
+ * (`bins`, a positive integer, defaults to `1024`), number of values to
1373
+ * sample (`sample`; omitting it, `0`, or at least the number of values
1374
+ * disables sampling), and seed for sampling (`seed`, defaults to `0`)
1375
+ * @returns A promise that resolves to the histogram, as bin counts (`hist`)
1376
+ * and the given value range (`range`)
1377
+ */
1378
+ static async computeHistogram(values, range, options) {
1379
+ const { signal, bins = 1024, sample, seed = 0 } = options ?? {};
1380
+ signal?.throwIfAborted();
1381
+ const [vmin, vmax] = range;
1382
+ const hist = new Array(bins).fill(0);
1383
+ const scale = vmin < vmax ? (bins - 1) / (vmax - vmin) : 0;
1384
+ const rng = sample !== void 0 && sample > 0 && sample < values.length ? RandomUtils.createUint32RNG(seed) : void 0;
1385
+ await AsyncUtils.forEach({ length: Math.min(sample || values.length, values.length) }, (_, i) => {
1386
+ const v = values[rng ? rng() % values.length : i];
1387
+ if (Number.isFinite(v)) {
1388
+ const bin = MathUtils.clamp(Math.round((v - vmin) * scale), 0, bins - 1);
1389
+ hist[bin] += 1;
1390
+ }
1391
+ }, { signal });
1392
+ return {
1393
+ hist,
1394
+ range
1395
+ };
1396
+ }
1397
+ /**
1398
+ * Redistributes the counts of a histogram over equal bins of another range
1399
+ *
1400
+ * The new bins split `range` into `bins` equal intervals, the upper bound
1401
+ * counting in the last one. Each bin of `histogram` is counted in the new bin
1402
+ * that its value falls into (see {@link computeHistogram}), or dropped if
1403
+ * its value is outside `range`. If `range` is degenerate (upper bound not
1404
+ * above lower bound), all counts fall into bin `0`.
1405
+ *
1406
+ * @param histogram - The histogram to redistribute, as bin counts (`hist`)
1407
+ * and the value range they span (`range`)
1408
+ * @param range - The value range the new bins span, as `[min, max]`
1409
+ * @param bins - The number of new bins, a positive integer
1410
+ * @returns The redistributed histogram, as the counts of the new bins
1411
+ * (`hist`) and the given value range (`range`)
1412
+ */
1413
+ static rebinHistogram(histogram, range, bins) {
1414
+ const { hist: origHist, range: origRange } = histogram;
1415
+ const [min, max] = range;
1416
+ const lastHistogramBin = Math.max(origHist.length - 1, 1);
1417
+ const hist = new Array(bins).fill(0);
1418
+ if (min >= max) {
1419
+ hist[0] = origHist.reduce((sum, count) => sum + count, 0);
1420
+ return {
1421
+ hist,
1422
+ range
1423
+ };
1424
+ }
1425
+ for (let i = 0; i < origHist.length; i++) {
1426
+ const value = MathUtils.remap(i, [0, lastHistogramBin], origRange);
1427
+ if (value >= min && value <= max) {
1428
+ const bin = Math.floor(MathUtils.remap(value, range, [0, bins]));
1429
+ hist[Math.min(bin, bins - 1)] += origHist[i];
1430
+ }
1431
+ }
1432
+ return {
1433
+ hist,
1434
+ range
1435
+ };
1436
+ }
1437
+ /**
1438
+ * Counts the number of occurrences of every distinct value
1439
+ *
1440
+ * The counts are keyed by value, in the order the values first occur.
1441
+ * Distinctness follows `Map` key equality, i.e. `NaN` counts as one value
1442
+ * and `0` and `-0` as the same one.
1443
+ *
1444
+ * The values are traversed on the main thread, yielding to the event loop
1445
+ * periodically so the UI stays responsive for large arrays (see
1446
+ * {@link AsyncUtils.forEach}). Aborting the signal rejects with its reason.
1447
+ *
1448
+ * @typeParam T - Element type of the values
1449
+ * @param values - The values to count
1450
+ * @param options - Optional abort signal
1451
+ * @returns A promise that resolves to the count of every distinct value, in
1452
+ * the order the values first appear
1453
+ */
1454
+ static async computeUniqueValueCounts(values, options) {
1455
+ const { signal } = options ?? {};
1456
+ signal?.throwIfAborted();
1457
+ const counts = /* @__PURE__ */ new Map();
1458
+ await AsyncUtils.forEach(values, (v) => {
1459
+ counts.set(v, (counts.get(v) ?? 0) + 1);
1460
+ }, { signal });
1461
+ return counts;
1462
+ }
1463
+ };
1464
+ //#endregion
1465
+ //#region src/utils/ColorUtils.ts
1466
+ /** Utility methods for color parsing, packing, and conversion */
1467
+ var ColorUtils = class {
1468
+ /**
1469
+ * Parses a text-based color palette into an array of colors
1470
+ *
1471
+ * Each non-empty line is expected to contain three numeric components
1472
+ * separated by `sep`, scaled from `[0, maxValue]` to `[0, 255]`.
1473
+ *
1474
+ * @param str - Multi-line palette string
1475
+ * @param options - Component separator (default `" "`) and maximum input
1476
+ * value per component (default `1`)
1477
+ * @returns The parsed colors, in the order of the palette's lines
1478
+ * @throws Error if a non-empty line does not hold exactly three components
1479
+ */
1480
+ static parsePalette(str, options) {
1481
+ const { sep = " ", maxValue = 1 } = options ?? {};
1482
+ return str.split("\n").filter((line) => line.trim().length > 0).map((line, i) => {
1483
+ const values = line.split(sep);
1484
+ if (values.length !== 3) throw new Error(`Invalid color palette line ${i}: ${line}`);
1485
+ return {
1486
+ r: Number(values[0]) / maxValue * 255,
1487
+ g: Number(values[1]) / maxValue * 255,
1488
+ b: Number(values[2]) / maxValue * 255
1489
+ };
1490
+ });
1491
+ }
1492
+ /**
1493
+ * Samples a continuous color scheme into an array of colors
1494
+ *
1495
+ * The scheme is evaluated at `n` evenly spaced positions covering the whole
1496
+ * `[0, 1]` range, both ends included.
1497
+ *
1498
+ * @param colorScheme - Maps a position within `[0, 1]` to a CSS color string
1499
+ * @param n - Number of colors to sample, an integer of at least two
1500
+ * @returns The sampled colors, in the order of the scheme
1501
+ * @throws Error if `n` is not an integer of at least two, or if the scheme
1502
+ * yields a color string that cannot be parsed
1503
+ */
1504
+ static sampleColorScheme(colorScheme, n) {
1505
+ if (!Number.isInteger(n) || n < 2) throw new Error(`Invalid color scheme size: ${n}`);
1506
+ return Array.from({ length: n }, (_, i) => {
1507
+ const spec = colorScheme(i / (n - 1));
1508
+ const { r, g, b } = rgb(spec);
1509
+ if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) throw new Error(`Invalid color scheme color: ${spec}`);
1510
+ return {
1511
+ r,
1512
+ g,
1513
+ b
1514
+ };
1515
+ });
1516
+ }
1517
+ /**
1518
+ * Packs an RGB color into a single 24-bit integer, `0xBBGGRR`
1519
+ *
1520
+ * The red component occupies the lowest byte, so that the integer, when
1521
+ * stored in host byte order on a little-endian host, matches the R, G, B byte
1522
+ * order of a canvas `ImageData` buffer.
1523
+ *
1524
+ * @param color - The color to pack
1525
+ * @returns The packed color, `0xBBGGRR`
1526
+ */
1527
+ static packColor(color) {
1528
+ return color.b << 16 | color.g << 8 | color.r;
1529
+ }
1530
+ /**
1531
+ * Packs a visibility into the form that {@link withAlpha} folds into a color
1532
+ *
1533
+ * @param visibility - The visibility
1534
+ * @returns `1` if visible, `0` if not
1535
+ */
1536
+ static packVisibility(visibility) {
1537
+ return visibility ? 1 : 0;
1538
+ }
1539
+ /**
1540
+ * Packs an opacity into the form that {@link withAlpha} folds into a color
1541
+ *
1542
+ * @param opacity - The opacity, in the range [0, 1]; values outside are clamped
1543
+ * @returns The opacity as an integer in the range [0, 255]
1544
+ */
1545
+ static packOpacity(opacity) {
1546
+ return MathUtils.clamp(Math.round(opacity * 255), 0, 255);
1547
+ }
1548
+ /**
1549
+ * Folds a visibility and an opacity into the alpha channel of a packed color
1550
+ *
1551
+ * Combines the lower 24 bits of `color` (see {@link packColor}) with
1552
+ * `opacity` as alpha if `visibility` is non-zero, and with zero alpha
1553
+ * otherwise, into a packed 32-bit RGBA color, `0xAABBGGRR`.
1554
+ *
1555
+ * @param packedColor - The packed color, `0xBBGGRR`; any higher bits are discarded
1556
+ * @param packedVisibility - The visibility, larger than `0` for visible
1557
+ * @param packedOpacity - The opacity, in the range [0, 255]
1558
+ * @returns The packed RGBA color, `0xAABBGGRR`, as an unsigned 32-bit integer
1559
+ */
1560
+ static withAlpha(packedColor, packedVisibility, packedOpacity) {
1561
+ const rgb = packedColor & 16777215;
1562
+ if (packedVisibility > 0) return BitUtils.safeOr(rgb, BitUtils.safeLeftShift(packedOpacity, 24));
1563
+ return rgb;
1564
+ }
1565
+ /**
1566
+ * Parses a hex color string into a {@link Color}
1567
+ *
1568
+ * @param hex - A `#RRGGBB` hex string
1569
+ * @returns The parsed color
1570
+ * @throws Error if the string is not a valid 6-digit hex color
1571
+ */
1572
+ static fromHex(hex) {
1573
+ if (!/^#([0-9A-Fa-f]{6})$/.test(hex)) throw new Error(`Invalid hex color: ${hex}`);
1574
+ return {
1575
+ r: parseInt(hex.slice(1, 3), 16),
1576
+ g: parseInt(hex.slice(3, 5), 16),
1577
+ b: parseInt(hex.slice(5, 7), 16)
1578
+ };
1579
+ }
1580
+ /**
1581
+ * Formats a {@link Color} as a `#RRGGBB` hex string
1582
+ *
1583
+ * @param color - The color to format
1584
+ * @returns The formatted hex string
1585
+ */
1586
+ static toHex(color) {
1587
+ return `#${Math.round(color.r).toString(16).padStart(2, "0")}${Math.round(color.g).toString(16).padStart(2, "0")}${Math.round(color.b).toString(16).padStart(2, "0")}`;
1588
+ }
1589
+ /**
1590
+ * Checks whether two colors have identical RGB components
1591
+ *
1592
+ * Components are compared exactly, without rounding or tolerance.
1593
+ *
1594
+ * @param a - The first color
1595
+ * @param b - The second color
1596
+ * @returns `true` if all three components are equal, `false` otherwise
1597
+ */
1598
+ static equals(a, b) {
1599
+ return a.r === b.r && a.g === b.g && a.b === b.b;
1600
+ }
1601
+ /**
1602
+ * Converts an HSB (HSV) color to RGB, following Java's `Color.HSBtoRGB`
1603
+ *
1604
+ * @param hue - The hue, in the range [0, 1) (values outside wrap around)
1605
+ * @param saturation - The saturation, in the range [0, 1]
1606
+ * @param brightness - The brightness, in the range [0, 1]
1607
+ * @returns The RGB color, with integer components in the range [0, 255]
1608
+ */
1609
+ static fromHSB(hue, saturation, brightness) {
1610
+ const scale = (x) => Math.round(x * 255);
1611
+ if (saturation === 0) {
1612
+ const v = scale(brightness);
1613
+ return {
1614
+ r: v,
1615
+ g: v,
1616
+ b: v
1617
+ };
1618
+ }
1619
+ const h = (hue - Math.floor(hue)) * 6;
1620
+ const f = h - Math.floor(h);
1621
+ const v = scale(brightness);
1622
+ const p = scale(brightness * (1 - saturation));
1623
+ const q = scale(brightness * (1 - saturation * f));
1624
+ const t = scale(brightness * (1 - saturation * (1 - f)));
1625
+ switch (Math.floor(h)) {
1626
+ case 0: return {
1627
+ r: v,
1628
+ g: t,
1629
+ b: p
1630
+ };
1631
+ case 1: return {
1632
+ r: q,
1633
+ g: v,
1634
+ b: p
1635
+ };
1636
+ case 2: return {
1637
+ r: p,
1638
+ g: v,
1639
+ b: t
1640
+ };
1641
+ case 3: return {
1642
+ r: p,
1643
+ g: q,
1644
+ b: v
1645
+ };
1646
+ case 4: return {
1647
+ r: t,
1648
+ g: p,
1649
+ b: v
1650
+ };
1651
+ default: return {
1652
+ r: v,
1653
+ g: p,
1654
+ b: q
1655
+ };
1656
+ }
1657
+ }
1658
+ };
1659
+ //#endregion
1660
+ //#region src/utils/ConfigUtils.ts
1661
+ /** Utility methods for property configurations */
1662
+ var ConfigUtils = class ConfigUtils {
1663
+ /**
1664
+ * Returns the group-to-value map that a configuration resolves its values
1665
+ * from, if any
1666
+ *
1667
+ * Only an active `groupBy` source (see {@link getActiveConfigSource}) with a
1668
+ * map ID resolves from a map. The map is returned as found in `maps`, rather
1669
+ * than copied: maps are never mutated, as an edit replaces the map, so
1670
+ * callers can detect an edit by comparing the returned maps by identity.
1671
+ *
1672
+ * @param config - The configuration
1673
+ * @param maps - The project-global maps to look the referenced map up in
1674
+ * @returns The map, or `undefined` if the configuration does not resolve
1675
+ * from a map, or if the map it references does not exist
1676
+ */
1677
+ static findGroupByMap(config, maps) {
1678
+ if (getActiveConfigSource(config) === "groupBy" && isGroupByConfig(config) && config.groupBy.map !== void 0) return maps.find((map) => map.id === config.groupBy.map);
1679
+ }
1680
+ /**
1681
+ * Returns the IDs of the maps that configurations refer to
1682
+ *
1683
+ * A configuration refers to its map whatever its active source, as switching
1684
+ * back to `groupBy` uses the map again.
1685
+ *
1686
+ * @param configs - The configurations
1687
+ * @returns The IDs of the referenced maps
1688
+ */
1689
+ static getGroupByMapIds(configs) {
1690
+ const mapIds = /* @__PURE__ */ new Set();
1691
+ for (const config of configs) if (isGroupByConfig(config) && config.groupBy.map !== void 0) mapIds.add(config.groupBy.map);
1692
+ return mapIds;
1693
+ }
1694
+ /**
1695
+ * Creates the function that returns the value a group-by configuration
1696
+ * assigns to a group
1697
+ *
1698
+ * With a map, a group takes its value in the map, else the map's default,
1699
+ * else `defaultValue`; a map that does not exist gives every group
1700
+ * `defaultValue`. Without a map, a group takes the palette value its name
1701
+ * hashes to, or `defaultValue` if there is no palette.
1702
+ *
1703
+ * @param config - The group-by configuration
1704
+ * @param map - The map the configuration refers to (see
1705
+ * {@link findGroupByMap}), or `undefined` if it refers to none or the map
1706
+ * does not exist
1707
+ * @param defaultValue - The value of a group that nothing assigns one to
1708
+ * @param palette - The values to pick from by hash if there is no map
1709
+ * @returns The value of a group, by group name (the cell value as a string)
1710
+ */
1711
+ static createGroupValueGetter(config, map, defaultValue, palette) {
1712
+ if (config.groupBy.map !== void 0) {
1713
+ if (map === void 0) return () => defaultValue;
1714
+ const values = new Map(Object.entries(map.values));
1715
+ const mapDefault = map.default ?? defaultValue;
1716
+ return (group) => values.get(group) ?? mapDefault;
1717
+ }
1718
+ if (palette === void 0 || palette.length === 0) return () => defaultValue;
1719
+ return (group) => palette[HashUtils.hash(group) % palette.length];
1720
+ }
1721
+ /**
1722
+ * Returns the table column a configuration groups by
1723
+ *
1724
+ * @param config - The configuration
1725
+ * @returns The column, or `undefined` if `groupBy` is not the active source
1726
+ */
1727
+ static getGroupByColumn(config) {
1728
+ return getActiveConfigSource(config) === "groupBy" && isGroupByConfig(config) ? config.groupBy : void 0;
1729
+ }
1730
+ /**
1731
+ * Points a configuration at a map, grouping by a column
1732
+ *
1733
+ * The configuration keeps its other sources and the extra fields of its
1734
+ * `groupBy` specification (e.g. a palette), so that switching back to them
1735
+ * restores them. The unit of the active source, which only sizes have, is
1736
+ * carried over, so that the values keep their scale. A configuration that
1737
+ * already groups by the column with the map is returned as is, so that
1738
+ * updating an object with it changes nothing.
1739
+ *
1740
+ * @param config - The configuration
1741
+ * @param groupBy - The categorical table column to group by
1742
+ * @param mapId - ID of the project-global map to take the group values from
1743
+ * @returns The configuration, with `groupBy` as its active source
1744
+ */
1745
+ static withGroupByMap(config, groupBy, mapId) {
1746
+ const currentGroupBy = ConfigUtils.getGroupByColumn(config);
1747
+ if (currentGroupBy !== void 0 && ConfigUtils.isSameTableColumn(currentGroupBy, groupBy) && isGroupByConfig(config) && config.groupBy.map === mapId) return config;
1748
+ const prevGroupBy = isGroupByConfig(config) ? config.groupBy : void 0;
1749
+ const unit = ConfigUtils.getUnit(config);
1750
+ return {
1751
+ ...config,
1752
+ source: "groupBy",
1753
+ groupBy: {
1754
+ ...prevGroupBy,
1755
+ ...(unit !== void 0 || prevGroupBy?.unit !== void 0) && { unit },
1756
+ table: groupBy.table,
1757
+ column: groupBy.column,
1758
+ map: mapId
1759
+ }
1760
+ };
1761
+ }
1762
+ /**
1763
+ * Returns the unit of the active source of a configuration
1764
+ *
1765
+ * @param config - The configuration, of which only sizes have a unit
1766
+ * @returns The unit, or `undefined` if the active source has none
1767
+ */
1768
+ static getUnit(config) {
1769
+ switch (getActiveConfigSource(config)) {
1770
+ case "constant": return isConstantConfig(config) ? config.constant.unit : void 0;
1771
+ case "from": return isFromConfig(config) ? config.from.unit : void 0;
1772
+ case "groupBy": return isGroupByConfig(config) ? config.groupBy.unit : void 0;
1773
+ default: return;
1774
+ }
1775
+ }
1776
+ /**
1777
+ * Determines whether two table column references point to the same column
1778
+ *
1779
+ * References without a table point to the column of `defaultTable`.
1780
+ *
1781
+ * @param tableColumnRef - The table column reference
1782
+ * @param otherTableColumnRef - The table column reference to compare with
1783
+ * @param defaultTable - ID of the table of references without a table
1784
+ * @returns Whether both references point to the same column
1785
+ */
1786
+ static isSameTableColumn(tableColumnRef, otherTableColumnRef, defaultTable) {
1787
+ return (tableColumnRef.table ?? defaultTable) === (otherTableColumnRef.table ?? defaultTable) && tableColumnRef.column === otherTableColumnRef.column;
1788
+ }
1789
+ };
1790
+ //#endregion
1791
+ //#region src/utils/GeometryUtils.ts
1792
+ /** Utility methods for rectangle and dimension geometry */
1793
+ var GeometryUtils = class {
1794
+ /**
1795
+ * Computes the union of multiple rectangles and returns the smallest rectangle that contains all of them
1796
+ *
1797
+ * @param rects - The rectangles to compute the union of
1798
+ * @returns The smallest rectangle that contains all the input rectangles, or `null` if no rectangles are provided
1799
+ */
1800
+ static union(...rects) {
1801
+ return rects.reduce((union, rect) => {
1802
+ if (union === null) return rect;
1803
+ const x = Math.min(union.x, rect.x);
1804
+ const y = Math.min(union.y, rect.y);
1805
+ return {
1806
+ x,
1807
+ y,
1808
+ width: Math.max(union.x + union.width, rect.x + rect.width) - x,
1809
+ height: Math.max(union.y + union.height, rect.y + rect.height) - y
1810
+ };
1811
+ }, null);
1812
+ }
1813
+ /**
1814
+ * Computes the intersection of two rectangles
1815
+ *
1816
+ * @param a - The first rectangle
1817
+ * @param b - The second rectangle
1818
+ * @returns The rectangle covered by both, or `null` if they do not overlap
1819
+ * (touching edges do not count as overlapping)
1820
+ */
1821
+ static intersection(a, b) {
1822
+ const x = Math.max(a.x, b.x);
1823
+ const y = Math.max(a.y, b.y);
1824
+ const width = Math.min(a.x + a.width, b.x + b.width) - x;
1825
+ const height = Math.min(a.y + a.height, b.y + b.height) - y;
1826
+ if (width <= 0 || height <= 0) return null;
1827
+ return {
1828
+ x,
1829
+ y,
1830
+ width,
1831
+ height
1832
+ };
1833
+ }
1834
+ /**
1835
+ * Grows a rectangle by the same amount on every side
1836
+ *
1837
+ * @param rect - The rectangle to grow
1838
+ * @param amount - The amount to move every side outwards by; negative
1839
+ * amounts shrink the rectangle
1840
+ * @returns The grown rectangle
1841
+ */
1842
+ static dilate(rect, amount) {
1843
+ return {
1844
+ x: rect.x - amount,
1845
+ y: rect.y - amount,
1846
+ width: rect.width + 2 * amount,
1847
+ height: rect.height + 2 * amount
1848
+ };
1849
+ }
1850
+ /**
1851
+ * Checks if two dimensions are equal by comparing their width and height properties
1852
+ *
1853
+ * @param a - The first dimensions to compare
1854
+ * @param b - The second dimensions to compare
1855
+ * @returns `true` if the dimensions are equal, `false` otherwise
1856
+ */
1857
+ static dimsEquals(a, b) {
1858
+ return a.width === b.width && a.height === b.height;
1859
+ }
1860
+ /**
1861
+ * Checks if two rectangles are equal by comparing their x, y, width, and height properties
1862
+ *
1863
+ * @param a - The first rectangle to compare
1864
+ * @param b - The second rectangle to compare
1865
+ * @returns `true` if the rectangles are equal, `false` otherwise
1866
+ */
1867
+ static rectEquals(a, b) {
1868
+ return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
1869
+ }
1870
+ };
1871
+ //#endregion
1872
+ //#region src/utils/ImageUtils.ts
1873
+ /** The integer typed arrays, with the bits and signedness of their type */
1874
+ var integerArrayTypes = [
1875
+ [
1876
+ Uint8Array,
1877
+ 8,
1878
+ false
1879
+ ],
1880
+ [
1881
+ Uint8ClampedArray,
1882
+ 8,
1883
+ false
1884
+ ],
1885
+ [
1886
+ Uint16Array,
1887
+ 16,
1888
+ false
1889
+ ],
1890
+ [
1891
+ Uint32Array,
1892
+ 32,
1893
+ false
1894
+ ],
1895
+ [
1896
+ Int8Array,
1897
+ 8,
1898
+ true
1899
+ ],
1900
+ [
1901
+ Int16Array,
1902
+ 16,
1903
+ true
1904
+ ],
1905
+ [
1906
+ Int32Array,
1907
+ 32,
1908
+ true
1909
+ ]
1910
+ ];
1911
+ /** Utility methods for resolving and defaulting image channel settings */
1912
+ var ImageUtils = class ImageUtils {
1913
+ /**
1914
+ * Returns the color of a channel
1915
+ *
1916
+ * The color configured on the image wins, then the one its data states, and
1917
+ * the only channel of single-channel image data is white, as a grayscale
1918
+ * image rather than in the red the index-based default would give it (see
1919
+ * {@link ImageUtils.getDefaultChannelColor}).
1920
+ *
1921
+ * @param image - The image
1922
+ * @param data - The image data providing the channel
1923
+ * @param c - The channel index (0-based)
1924
+ * @returns The channel's color
1925
+ */
1926
+ static getChannelColor(image, data, c) {
1927
+ return image.channels?.[c]?.color ?? data.getChannelColor?.(c) ?? (data.getSizeC() === 1 ? {
1928
+ r: 255,
1929
+ g: 255,
1930
+ b: 255
1931
+ } : ImageUtils.getDefaultChannelColor(c));
1932
+ }
1933
+ /**
1934
+ * Returns the contrast limits of a channel
1935
+ *
1936
+ * The limits configured on the image win over those its data states.
1937
+ * Channels with neither are stretched between quantile-based limits derived
1938
+ * from their histogram (see {@link ImageUtils.getDefaultContrastLimits}), if
1939
+ * the data provides one, and otherwise over the range the data declares for
1940
+ * their data type.
1941
+ *
1942
+ * @param image - The image
1943
+ * @param data - The image data providing the channel
1944
+ * @param c - The channel index (0-based)
1945
+ * @returns The channel's contrast limits, or `undefined` if the data
1946
+ * provides neither limits, a histogram nor a data type range
1947
+ */
1948
+ static getChannelContrastLimits(image, data, c) {
1949
+ const histogram = data.getChannelHistogram?.(c);
1950
+ return image.channels?.[c]?.contrastLimits ?? data.getChannelContrastLimits?.(c) ?? (histogram !== void 0 ? ImageUtils.getDefaultContrastLimits(histogram) : void 0) ?? data.getChannelDataTypeRange?.(c);
1951
+ }
1952
+ /**
1953
+ * Returns the visibility of a channel
1954
+ *
1955
+ * The visibility configured on the image wins over the one its data states,
1956
+ * and channels with neither are visible.
1957
+ *
1958
+ * @param image - The image
1959
+ * @param data - The image data providing the channel
1960
+ * @param c - The channel index (0-based)
1961
+ * @returns Whether the channel is visible
1962
+ */
1963
+ static getChannelVisibility(image, data, c) {
1964
+ return image.channels?.[c]?.visibility ?? data.getChannelVisibility?.(c) ?? true;
1965
+ }
1966
+ /**
1967
+ * Returns the opacity of a channel
1968
+ *
1969
+ * The opacity configured on the image wins over the one its data states,
1970
+ * and channels with neither are fully opaque.
1971
+ *
1972
+ * @param image - The image
1973
+ * @param data - The image data providing the channel
1974
+ * @param c - The channel index (0-based)
1975
+ * @returns The channel's opacity, in `[0, 1]`
1976
+ */
1977
+ static getChannelOpacity(image, data, c) {
1978
+ return image.channels?.[c]?.opacity ?? data.getChannelOpacity?.(c) ?? 1;
1979
+ }
1980
+ /**
1981
+ * Returns the channel an image shows in the single-channel view modes
1982
+ *
1983
+ * The image's active channel is bounded to the channels the data has, so
1984
+ * that an image kept from data with more channels still shows one.
1985
+ *
1986
+ * @param image - The image
1987
+ * @param sizeC - The number of channels the image data has
1988
+ * @returns The channel index (0-based)
1989
+ */
1990
+ static getActiveChannel(image, sizeC) {
1991
+ return MathUtils.clamp(image.activeChannel, 0, sizeC - 1);
1992
+ }
1993
+ /**
1994
+ * Returns a default color for a channel, for use when no channel colors are
1995
+ * known
1996
+ *
1997
+ * The first six channels map to red, green, blue, yellow, cyan, and magenta.
1998
+ * Further channels are assigned hues 128 degrees apart, with saturation and
1999
+ * brightness decreasing by 0.05 every ten channels. Channel indices wrap
2000
+ * around after 100, so that saturation and brightness stay above 0.5.
2001
+ * Colors are not guaranteed to be unique.
2002
+ *
2003
+ * Resembles `ImageChannel.getDefaultChannelColor` in QuPath v0.7.0, except
2004
+ * that yellow, cyan, and magenta have been replaced by pure versions (255
2005
+ * for the two highest components, 0 for the lowest), and that QuPath wraps
2006
+ * after 360 channels instead, yielding dim colors and eventually black.
2007
+ *
2008
+ * @param c - The channel index, a non-negative integer
2009
+ * @returns The default color for the channel
2010
+ */
2011
+ static getDefaultChannelColor(c) {
2012
+ c = c % 100;
2013
+ switch (c) {
2014
+ case 0: return {
2015
+ r: 255,
2016
+ g: 0,
2017
+ b: 0
2018
+ };
2019
+ case 1: return {
2020
+ r: 0,
2021
+ g: 255,
2022
+ b: 0
2023
+ };
2024
+ case 2: return {
2025
+ r: 0,
2026
+ g: 0,
2027
+ b: 255
2028
+ };
2029
+ case 3: return {
2030
+ r: 255,
2031
+ g: 255,
2032
+ b: 0
2033
+ };
2034
+ case 4: return {
2035
+ r: 0,
2036
+ g: 255,
2037
+ b: 255
2038
+ };
2039
+ case 5: return {
2040
+ r: 255,
2041
+ g: 0,
2042
+ b: 255
2043
+ };
2044
+ default: {
2045
+ const hue = c * 128 % 360 / 360;
2046
+ const level = 1 - Math.floor(c / 10) / 20;
2047
+ return ColorUtils.fromHSB(hue, level, level);
2048
+ }
2049
+ }
2050
+ }
2051
+ /**
2052
+ * Returns quantile-based contrast limits derived from a channel histogram,
2053
+ * for use as default contrast limits when no contrast limits are known
2054
+ *
2055
+ * The histogram's `hist[i]` counts the values that fall into bin `i`, with
2056
+ * bins spread evenly over `range`: bin `0` maps to the range's lower bound,
2057
+ * the last bin to its upper bound, and each bin in between to
2058
+ * `vmin + i / (n - 1) * (vmax - vmin)` (see
2059
+ * {@link MathUtils.computeHistogram}). The lower limit is the value of the
2060
+ * first bin at which the cumulative count from the bottom reaches the
2061
+ * fraction `qlow` of the total count, the upper limit is the value of the
2062
+ * first bin at which the cumulative count from the top reaches the fraction
2063
+ * `1 - qhigh`; i.e., the fraction `qlow` of the values is clipped at the
2064
+ * bottom and the fraction `1 - qhigh` at the top. Only non-empty bins are
2065
+ * considered, so quantiles of `0` and `1` yield the first and last non-empty
2066
+ * bin, respectively. Values that fall into the same bin cannot be told
2067
+ * apart, so the limits are only as precise as the bins.
2068
+ *
2069
+ * Histograms with fewer than two bins, histograms whose counts sum to zero
2070
+ * and degenerate ranges return `range` as is. So do histograms whose clipped
2071
+ * limits would not be ascending (upper bin at or below lower bin), which
2072
+ * happens if more than `qhigh - qlow` of the values fall into a single bin,
2073
+ * e.g. for sparse channels that are mostly background, or if `qhigh` is
2074
+ * below `qlow`, and a `qlow` above `1`, which no bin can reach.
2075
+ *
2076
+ * @param histogram - The channel histogram, as bin counts and value range
2077
+ * @param qlow - The quantile of the lower limit, in `[0, 1]`
2078
+ * @param qhigh - The quantile of the upper limit, in `[0, 1]`, above `qlow`
2079
+ * @returns The contrast limits, as `[low, high]` in the histogram's value
2080
+ * range, with `low < high` unless the range is degenerate
2081
+ */
2082
+ static getDefaultContrastLimits(histogram, qlow = .01, qhigh = .999) {
2083
+ const { hist, range: [vmin, vmax] } = histogram;
2084
+ const n = hist.length;
2085
+ if (n < 2 || vmin === vmax) return [vmin, vmax];
2086
+ const total = hist.reduce((sum, count) => sum + count, 0);
2087
+ if (total === 0) return [vmin, vmax];
2088
+ let binLeft;
2089
+ let binRight;
2090
+ let cumulativeLeft = 0;
2091
+ let cumulativeRight = 0;
2092
+ const targetLeft = total * qlow;
2093
+ const targetRight = total - total * qhigh;
2094
+ for (let i = 0; i < n; i++) {
2095
+ const countLeft = hist[i];
2096
+ if (countLeft > 0) {
2097
+ cumulativeLeft += countLeft;
2098
+ if (binLeft === void 0 && cumulativeLeft >= targetLeft) binLeft = i;
2099
+ }
2100
+ const countRight = hist[n - 1 - i];
2101
+ if (countRight > 0) {
2102
+ cumulativeRight += countRight;
2103
+ if (binRight === void 0 && cumulativeRight >= targetRight) binRight = n - 1 - i;
2104
+ }
2105
+ if (binLeft !== void 0 && binRight !== void 0) break;
2106
+ }
2107
+ if (binLeft === void 0 || binRight === void 0 || binLeft >= binRight) return [vmin, vmax];
2108
+ return [vmin + binLeft / (n - 1) * (vmax - vmin), vmin + binRight / (n - 1) * (vmax - vmin)];
2109
+ }
2110
+ /**
2111
+ * Returns the value range that an integer type can hold
2112
+ *
2113
+ * @param bits - The number of bits per value, at most 32
2114
+ * @param signed - Whether the type is signed
2115
+ * @returns The value range, as `[min, max]`
2116
+ */
2117
+ static getIntegerTypeRange(bits, signed) {
2118
+ return signed ? [-(2 ** (bits - 1)), 2 ** (bits - 1) - 1] : [0, 2 ** bits - 1];
2119
+ }
2120
+ /**
2121
+ * Returns the value range that the type of the given array can hold, for use
2122
+ * as default contrast limits of image channel data
2123
+ *
2124
+ * Integer typed arrays span their full integer range, floating-point typed
2125
+ * arrays are taken to hold normalized values in `[0, 1]`.
2126
+ *
2127
+ * @param values - The array whose value range to return
2128
+ * @returns The value range, as `[min, max]`
2129
+ */
2130
+ static getDataTypeRange(values) {
2131
+ for (const [arrayType, bits, signed] of integerArrayTypes) if (values instanceof arrayType) return ImageUtils.getIntegerTypeRange(bits, signed);
2132
+ return [0, 1];
2133
+ }
2134
+ };
2135
+ //#endregion
2136
+ //#region src/utils/JSONUtils.ts
2137
+ /**
2138
+ * Utility methods for JSON serialization with support for non-finite numbers
2139
+ *
2140
+ * `NaN`, `Infinity` and `-Infinity` are encoded as the sentinel strings
2141
+ * `"__NaN__"`, `"__PositiveInfinity__"` and `"__NegativeInfinity__"`, which
2142
+ * {@link JSONUtils.parse} decodes back into numbers. Strings that happen to
2143
+ * equal a sentinel are therefore not round-trip safe.
2144
+ */
2145
+ var JSONUtils = class JSONUtils {
2146
+ /**
2147
+ * Serializes a value to JSON, encoding non-finite numbers as sentinel strings
2148
+ *
2149
+ * Behaves like `JSON.stringify` without indentation: `toJSON` methods are
2150
+ * honored, and object properties that cannot be represented (`undefined`,
2151
+ * functions, symbols) are omitted.
2152
+ *
2153
+ * It differs from `JSON.stringify` where the latter is lossy: such a value
2154
+ * throws instead of yielding `undefined` when it is the top-level value, and
2155
+ * throws instead of being written as `null` when it is an array element. It
2156
+ * also does not detect circular references, which overflow the stack rather
2157
+ * than throwing a `TypeError`.
2158
+ *
2159
+ * @param value - The value to serialize
2160
+ * @param options - Optional `stable` flag, sorting object keys alphabetically
2161
+ * so that the output is independent of property insertion order
2162
+ * (default `false`)
2163
+ * @throws If the value itself, or any array element it contains, cannot be
2164
+ * represented in JSON
2165
+ */
2166
+ static stringify(value, options) {
2167
+ const { stable = false } = options || {};
2168
+ if (!JSONUtils._isStringifiable(value)) throw new Error(`Value of type ${typeof value} is not stringifiable`);
2169
+ if (typeof value !== "object" || value === null) return JSONUtils._stringifyPrimitive(value);
2170
+ const obj = value;
2171
+ if (typeof obj.toJSON === "function") return JSONUtils.stringify(obj.toJSON(), options);
2172
+ if (Array.isArray(value)) return `[${value.map((v) => JSONUtils.stringify(v, options)).join(",")}]`;
2173
+ const keys = Object.keys(obj).filter((key) => JSONUtils._isStringifiable(obj[key]));
2174
+ if (stable) keys.sort();
2175
+ return `{${keys.map((k) => `${JSONUtils._stringifyPrimitive(k)}:${JSONUtils.stringify(obj[k], options)}`).join(",")}}`;
2176
+ }
2177
+ /**
2178
+ * Parses JSON, decoding the sentinel strings written by
2179
+ * {@link JSONUtils.stringify} back into `NaN`, `Infinity` and `-Infinity`
2180
+ *
2181
+ * @param text - The JSON text to parse
2182
+ * @throws If the text is not valid JSON
2183
+ */
2184
+ static parse(text) {
2185
+ return JSON.parse(text, function(_key, value) {
2186
+ if (typeof value === "string") {
2187
+ if (value === "__NaN__") return NaN;
2188
+ if (value === "__PositiveInfinity__") return Number.POSITIVE_INFINITY;
2189
+ if (value === "__NegativeInfinity__") return Number.NEGATIVE_INFINITY;
2190
+ }
2191
+ return value;
2192
+ });
2193
+ }
2194
+ /** Whether a value can be represented in JSON */
2195
+ static _isStringifiable(value) {
2196
+ return value !== void 0 && typeof value !== "function" && typeof value !== "symbol";
2197
+ }
2198
+ /**
2199
+ * Serializes a single primitive value (or object key) using
2200
+ * `JSON.stringify`, replacing non-finite numbers with sentinel strings
2201
+ *
2202
+ * The replacer reads the raw value from the holder object, because
2203
+ * `JSON.stringify` would otherwise pass the already-converted value (`null`
2204
+ * for non-finite numbers) to the replacer.
2205
+ */
2206
+ static _stringifyPrimitive(value) {
2207
+ return JSON.stringify(value, function(key) {
2208
+ const rawValue = this[key];
2209
+ if (typeof rawValue === "number") {
2210
+ if (Number.isNaN(rawValue)) return "__NaN__";
2211
+ if (rawValue === Infinity) return "__PositiveInfinity__";
2212
+ if (rawValue === -Infinity) return "__NegativeInfinity__";
2213
+ }
2214
+ return rawValue;
2215
+ });
2216
+ }
2217
+ };
2218
+ //#endregion
2219
+ //#region src/utils/ProjectUtils.ts
2220
+ /** Utility methods for projects */
2221
+ var ProjectUtils = class {
2222
+ /**
2223
+ * Returns whether a project has any data objects
2224
+ *
2225
+ * @param project - The project, or only its data objects
2226
+ * @returns `true` if the project has images, labels, points, shapes or tables
2227
+ */
2228
+ static hasData(project) {
2229
+ return project.images.length > 0 || project.labels.length > 0 || project.points.length > 0 || project.shapes.length > 0 || project.tables.length > 0;
2230
+ }
2231
+ /**
2232
+ * Returns the marker configurations of a project's points
2233
+ *
2234
+ * @param project - The project, or only its labels, points and shapes
2235
+ * @returns The marker configurations
2236
+ */
2237
+ static getMarkerConfigs(project) {
2238
+ return project.points.map((points) => points.pointMarker);
2239
+ }
2240
+ /**
2241
+ * Returns the size configurations of a project's points
2242
+ *
2243
+ * @param project - The project, or only its labels, points and shapes
2244
+ * @returns The size configurations
2245
+ */
2246
+ static getSizeConfigs(project) {
2247
+ return project.points.map((points) => points.pointSize);
2248
+ }
2249
+ /**
2250
+ * Returns the color configurations of a project's labels, points and shapes
2251
+ *
2252
+ * @param project - The project, or only its labels, points and shapes
2253
+ * @returns The color configurations, including the fill and stroke colors of
2254
+ * shapes
2255
+ */
2256
+ static getColorConfigs(project) {
2257
+ return [
2258
+ ...project.labels.map((labels) => labels.labelColor),
2259
+ ...project.points.map((points) => points.pointColor),
2260
+ ...project.shapes.flatMap((shapes) => [shapes.shapeFillColor, shapes.shapeStrokeColor])
2261
+ ];
2262
+ }
2263
+ /**
2264
+ * Returns the visibility configurations of a project's labels, points and
2265
+ * shapes
2266
+ *
2267
+ * @param project - The project, or only its labels, points and shapes
2268
+ * @returns The visibility configurations, including the shape, fill and
2269
+ * stroke visibilities of shapes
2270
+ */
2271
+ static getVisibilityConfigs(project) {
2272
+ return [
2273
+ ...project.labels.map((labels) => labels.labelVisibility),
2274
+ ...project.points.map((points) => points.pointVisibility),
2275
+ ...project.shapes.flatMap((shapes) => [
2276
+ shapes.shapeVisibility,
2277
+ shapes.shapeFillVisibility,
2278
+ shapes.shapeStrokeVisibility
2279
+ ])
2280
+ ];
2281
+ }
2282
+ /**
2283
+ * Returns the opacity configurations of a project's labels, points and shapes
2284
+ *
2285
+ * @param project - The project, or only its labels, points and shapes
2286
+ * @returns The opacity configurations, including the shape, fill and stroke
2287
+ * opacities of shapes
2288
+ */
2289
+ static getOpacityConfigs(project) {
2290
+ return [
2291
+ ...project.labels.map((labels) => labels.labelOpacity),
2292
+ ...project.points.map((points) => points.pointOpacity),
2293
+ ...project.shapes.flatMap((shapes) => [
2294
+ shapes.shapeOpacity,
2295
+ shapes.shapeFillOpacity,
2296
+ shapes.shapeStrokeOpacity
2297
+ ])
2298
+ ];
2299
+ }
2300
+ };
2301
+ //#endregion
2302
+ //#region src/utils/SourceUtils.ts
2303
+ /**
2304
+ * Utility methods for resolving the `source` of data sources to URLs or file
2305
+ * system handles
2306
+ *
2307
+ * A source is one of the following, tried in this order:
2308
+ *
2309
+ * 1. A URL with a scheme (e.g. `https://…`, `blob:…`, `data:…`), which is
2310
+ * normalized and returned as is.
2311
+ * 2. An app-relative path, prefixed with `//`, resolved against the base URL
2312
+ * of the application (e.g. `//data/points.csv`).
2313
+ * 3. A workspace-relative path, prefixed with `/`, resolved to a file or
2314
+ * directory within the open workspace (e.g. `/data/points.csv`). Without an
2315
+ * open workspace it falls back to being app-relative.
2316
+ * 4. A project-relative path, without prefix, resolved against where the
2317
+ * project was loaded from: a URL, or a workspace-relative path (e.g.
2318
+ * `points.csv`, `./points.csv`, `../shared/points.csv`). Without a project
2319
+ * source it falls back to being workspace-relative. Whenever it resolves to
2320
+ * a workspace-relative path - against a workspace-relative project source,
2321
+ * or through that fallback - it falls back further to being app-relative if
2322
+ * no workspace is open.
2323
+ *
2324
+ * Resolution happens in two steps:
2325
+ *
2326
+ * 1. {@link SourceUtils.normalizeSource} classifies the source, applies the
2327
+ * fallbacks and normalizes it, synchronously and without touching the file
2328
+ * system. The result is either an absolute URL or a workspace-relative
2329
+ * path. Sources referring to the same data normalize to the same string,
2330
+ * and normalizing a normalized source yields it unchanged.
2331
+ * 2. {@link SourceUtils.resolveSource} opens the file or directory that a
2332
+ * normalized workspace-relative path refers to;
2333
+ * {@link SourceUtils.resolveSourceFile} and
2334
+ * {@link SourceUtils.resolveSourceDirectory} accept only one of the two,
2335
+ * and {@link SourceUtils.openSourceFile} also opens the file.
2336
+ * URLs need no such step and are returned as is.
2337
+ *
2338
+ * Paths use `/` as separator and may contain `.` and `..` segments. Resolving
2339
+ * against a URL follows URL semantics: `..` segments beyond the root of the
2340
+ * URL's path are dropped silently. Resolving within the workspace collapses the
2341
+ * segments and fails if the path leaves the workspace.
2342
+ *
2343
+ * A relative path whose first segment contains a colon (e.g.
2344
+ * `sample1:ch2.tif`) is taken for a URL; write it as `./sample1:ch2.tif`
2345
+ * instead.
2346
+ */
2347
+ var SourceUtils = class SourceUtils {
2348
+ static _pathSep = "/";
2349
+ static _appPathPrefix = "//";
2350
+ static _workspacePathPrefix = SourceUtils._pathSep;
2351
+ static _urlSchemePattern = /^[a-z][a-z0-9+.-]*:/i;
2352
+ /**
2353
+ * Returns whether a normalized source refers to a file or directory within
2354
+ * the workspace
2355
+ *
2356
+ * @param normalizedSource - The normalized source (see
2357
+ * {@link SourceUtils.normalizeSource})
2358
+ * @returns `true` for workspace-relative paths, `false` for URLs and
2359
+ * app-relative paths
2360
+ */
2361
+ static isWorkspacePath(normalizedSource) {
2362
+ return normalizedSource.startsWith(SourceUtils._workspacePathPrefix) && !normalizedSource.startsWith(SourceUtils._appPathPrefix);
2363
+ }
2364
+ /**
2365
+ * Builds a workspace-relative path from the segments of a path within the
2366
+ * workspace, as returned by `FileSystemDirectoryHandle.resolve`
2367
+ *
2368
+ * @param segments - The path segments, from the workspace root down to the
2369
+ * file or directory
2370
+ * @returns The workspace-relative path (with `/` prefix)
2371
+ */
2372
+ static makeWorkspacePath(segments) {
2373
+ return SourceUtils._workspacePathPrefix + segments.join(SourceUtils._pathSep);
2374
+ }
2375
+ /**
2376
+ * Returns the directory that contains a normalized source
2377
+ *
2378
+ * @param normalizedSource - The normalized source (see
2379
+ * {@link SourceUtils.normalizeSource})
2380
+ * @returns The normalized parent source and the name of the source within
2381
+ * it, decoded for URLs; `null` if the source is the root of its URL's path
2382
+ * or lies directly in the workspace, as the workspace root is no source
2383
+ */
2384
+ static getParentSource(normalizedSource) {
2385
+ if (SourceUtils.isWorkspacePath(normalizedSource)) {
2386
+ const segments = normalizedSource.substring(SourceUtils._workspacePathPrefix.length).split(SourceUtils._pathSep);
2387
+ const name = segments.pop();
2388
+ if (name === void 0 || segments.length === 0) return null;
2389
+ return {
2390
+ parentSource: SourceUtils.makeWorkspacePath(segments),
2391
+ name
2392
+ };
2393
+ }
2394
+ const url = new URL(normalizedSource);
2395
+ const segments = url.pathname.split(SourceUtils._pathSep).filter((segment) => segment !== "");
2396
+ const name = segments.pop();
2397
+ if (name === void 0) return null;
2398
+ url.pathname = segments.join(SourceUtils._pathSep);
2399
+ return {
2400
+ parentSource: url.toString(),
2401
+ name: decodeURIComponent(name)
2402
+ };
2403
+ }
2404
+ /**
2405
+ * Normalizes a source, applying the fallbacks for missing project sources
2406
+ * and workspaces
2407
+ *
2408
+ * @param source - The source to normalize
2409
+ * @param workspace - The directory handle of the open workspace, if any
2410
+ * @param projectSource - Where the project was loaded from: its absolute URL,
2411
+ * the workspace-relative path of the project file (with `/` prefix), or
2412
+ * `null` for projects that were loaded from neither (e.g. uploaded ones)
2413
+ * @param options - Optional base URL that app-relative paths are resolved
2414
+ * against (default `document.baseURI`); the application relies on the
2415
+ * default, and the option is what keeps normalization a pure function of
2416
+ * its arguments, testable without a `document`
2417
+ * @returns The absolute URL for URLs, app-relative paths, and paths that fall
2418
+ * back to being app-relative or resolve against a project URL; or the
2419
+ * normalized workspace-relative path (with `/` prefix) for paths within the
2420
+ * open workspace
2421
+ * @throws Error if the source is empty or not a valid URL, or if it cannot be
2422
+ * normalized as a project-relative, workspace-relative or app-relative path
2423
+ * (see {@link SourceUtils._normalizeProjectPath},
2424
+ * {@link SourceUtils._normalizeWorkspacePath} and
2425
+ * {@link SourceUtils._normalizeAppPath})
2426
+ */
2427
+ static normalizeSource(source, workspace, projectSource, options) {
2428
+ if (source === "") throw new Error("Empty source");
2429
+ if (SourceUtils._urlSchemePattern.test(source)) try {
2430
+ return new URL(source).toString();
2431
+ } catch (error) {
2432
+ throw new Error(`Invalid URL: ${source}`, { cause: error });
2433
+ }
2434
+ if (source.startsWith(SourceUtils._appPathPrefix)) return SourceUtils._normalizeAppPath(source, options);
2435
+ if (source.startsWith(SourceUtils._workspacePathPrefix)) return SourceUtils._normalizeWorkspacePath(source, workspace, options);
2436
+ return SourceUtils._normalizeProjectPath(source, workspace, projectSource, options);
2437
+ }
2438
+ /**
2439
+ * Resolves a normalized source to an absolute URL, a file handle or a
2440
+ * directory handle
2441
+ *
2442
+ * URLs are returned as is. A workspace-relative path is opened within the
2443
+ * workspace one directory at a time, and its last segment as a file, or as a
2444
+ * directory if it names one. No fallbacks apply here: the source has to be
2445
+ * normalized with {@link SourceUtils.normalizeSource} first, using the same
2446
+ * workspace. Nothing is thrown synchronously: the returned promise rejects
2447
+ * instead.
2448
+ *
2449
+ * @param normalizedSource - The normalized source to resolve: an absolute URL
2450
+ * or a workspace-relative path (with `/` prefix), as returned by
2451
+ * {@link SourceUtils.normalizeSource}
2452
+ * @param workspace - The directory handle of the open workspace, if any
2453
+ * @param options - Optional abort signal, checked upfront and between file
2454
+ * system calls
2455
+ * @returns A promise that resolves to the absolute URL, or to the file or
2456
+ * directory handle for sources within the workspace
2457
+ * @throws Error if a workspace-relative path is given without an open
2458
+ * workspace, or if the path leaves the workspace or names its root
2459
+ * @throws DOMException if a segment names nothing within the workspace
2460
+ * (`NotFoundError`), if a segment other than the last names a file
2461
+ * (`TypeMismatchError`), or if the operation is aborted (`AbortError`)
2462
+ */
2463
+ static async resolveSource(normalizedSource, workspace, options) {
2464
+ const { signal } = options ?? {};
2465
+ signal?.throwIfAborted();
2466
+ if (!SourceUtils.isWorkspacePath(normalizedSource)) return normalizedSource;
2467
+ if (workspace === null) throw new Error(`Cannot resolve workspace-relative path without workspace: ${normalizedSource}`);
2468
+ const segments = SourceUtils._collapseSegments(normalizedSource.substring(SourceUtils._workspacePathPrefix.length));
2469
+ const entryName = segments.pop();
2470
+ if (entryName === void 0) throw new Error(`Not a workspace file or directory: ${normalizedSource}`);
2471
+ let dir = workspace;
2472
+ for (const dirName of segments) {
2473
+ dir = await dir.getDirectoryHandle(dirName);
2474
+ signal?.throwIfAborted();
2475
+ }
2476
+ let entry;
2477
+ try {
2478
+ entry = await dir.getFileHandle(entryName);
2479
+ } catch (error) {
2480
+ if (!(error instanceof DOMException && error.name === "TypeMismatchError")) throw error;
2481
+ signal?.throwIfAborted();
2482
+ entry = await dir.getDirectoryHandle(entryName);
2483
+ }
2484
+ signal?.throwIfAborted();
2485
+ return entry;
2486
+ }
2487
+ /**
2488
+ * Resolves a normalized source to an absolute URL or a file handle
2489
+ *
2490
+ * Like {@link SourceUtils.resolveSource}, for sources that have to refer to
2491
+ * a file.
2492
+ *
2493
+ * @param normalizedSource - See {@link SourceUtils.resolveSource}
2494
+ * @param workspace - See {@link SourceUtils.resolveSource}
2495
+ * @param options - See {@link SourceUtils.resolveSource}
2496
+ * @returns A promise that resolves to the absolute URL, or to the file handle
2497
+ * for sources within the workspace
2498
+ * @throws See {@link SourceUtils.resolveSource}
2499
+ * @throws DOMException if the source refers to a directory within the
2500
+ * workspace (`TypeMismatchError`)
2501
+ */
2502
+ static async resolveSourceFile(normalizedSource, workspace, options) {
2503
+ const resolvedSource = await SourceUtils.resolveSource(normalizedSource, workspace, options);
2504
+ if (typeof resolvedSource !== "string" && resolvedSource.kind !== "file") throw new DOMException(`Not a workspace file: ${normalizedSource}`, "TypeMismatchError");
2505
+ return resolvedSource;
2506
+ }
2507
+ /**
2508
+ * Resolves a normalized source to an absolute URL or an opened file
2509
+ *
2510
+ * Like {@link SourceUtils.resolveSourceFile}, for readers that take either
2511
+ * a URL or a `File`: a file in the workspace is opened right away.
2512
+ *
2513
+ * @param normalizedSource - See {@link SourceUtils.resolveSource}
2514
+ * @param workspace - See {@link SourceUtils.resolveSource}
2515
+ * @param options - See {@link SourceUtils.resolveSource}; the signal is
2516
+ * also checked once the file is open, as opening it does not throw on
2517
+ * abort
2518
+ * @returns A promise that resolves to the absolute URL, or to the opened
2519
+ * file for sources within the workspace
2520
+ * @throws See {@link SourceUtils.resolveSourceFile}
2521
+ * @throws DOMException if the file cannot be opened, e.g. because it was
2522
+ * removed (`NotFoundError`) or its read permission was revoked
2523
+ * (`NotAllowedError`)
2524
+ * @throws The abort reason of the signal, if it is aborted once the file
2525
+ * is open
2526
+ */
2527
+ static async openSourceFile(normalizedSource, workspace, options) {
2528
+ const { signal } = options ?? {};
2529
+ const resolvedSource = await SourceUtils.resolveSourceFile(normalizedSource, workspace, options);
2530
+ if (typeof resolvedSource === "string") return { url: resolvedSource };
2531
+ const file = await resolvedSource.getFile();
2532
+ signal?.throwIfAborted();
2533
+ return { file };
2534
+ }
2535
+ /**
2536
+ * Resolves a normalized source to an absolute URL or a directory handle
2537
+ *
2538
+ * Like {@link SourceUtils.resolveSource}, for sources that have to refer to
2539
+ * a directory.
2540
+ *
2541
+ * @param normalizedSource - See {@link SourceUtils.resolveSource}
2542
+ * @param workspace - See {@link SourceUtils.resolveSource}
2543
+ * @param options - See {@link SourceUtils.resolveSource}
2544
+ * @returns A promise that resolves to the absolute URL, or to the directory
2545
+ * handle for sources within the workspace
2546
+ * @throws See {@link SourceUtils.resolveSource}
2547
+ * @throws DOMException if the source refers to a file within the workspace
2548
+ * (`TypeMismatchError`)
2549
+ */
2550
+ static async resolveSourceDirectory(normalizedSource, workspace, options) {
2551
+ const resolvedSource = await SourceUtils.resolveSource(normalizedSource, workspace, options);
2552
+ if (typeof resolvedSource !== "string" && resolvedSource.kind !== "directory") throw new DOMException(`Not a workspace directory: ${normalizedSource}`, "TypeMismatchError");
2553
+ return resolvedSource;
2554
+ }
2555
+ /**
2556
+ * Normalizes a project-relative path
2557
+ *
2558
+ * Against a project URL, the path is resolved to an absolute URL. Against a
2559
+ * workspace-relative project path, the path is resolved within the project
2560
+ * file's directory to a workspace-relative path. Without a project source,
2561
+ * the path is normalized as a workspace-relative path as is. Both of the
2562
+ * latter go through {@link SourceUtils._normalizeWorkspacePath}, including
2563
+ * its fallback for when no workspace is open.
2564
+ *
2565
+ * @param projectPath - The project-relative path (without prefix)
2566
+ * @param workspace - The directory handle of the open workspace, if any
2567
+ * @param projectSource - See {@link SourceUtils.normalizeSource}
2568
+ * @param options - Optional base URL for the app-relative fallback (see
2569
+ * {@link SourceUtils._normalizeAppPath})
2570
+ * @returns The absolute URL, or the normalized workspace-relative path
2571
+ * @throws Error if the path does not form a valid URL with the project URL,
2572
+ * if it leaves the workspace, or if it cannot be normalized as a
2573
+ * workspace-relative path (see {@link SourceUtils._normalizeWorkspacePath})
2574
+ */
2575
+ static _normalizeProjectPath(projectPath, workspace, projectSource, options) {
2576
+ if (projectSource !== null) {
2577
+ if (SourceUtils._urlSchemePattern.test(projectSource)) try {
2578
+ return new URL(projectPath, projectSource).toString();
2579
+ } catch (error) {
2580
+ throw new Error(`Invalid project-relative path: ${projectPath}`, { cause: error });
2581
+ }
2582
+ const projectDirSegments = SourceUtils._collapseSegments(projectSource.substring(SourceUtils._workspacePathPrefix.length)).slice(0, -1);
2583
+ const segments = SourceUtils._collapseSegments(projectPath, projectDirSegments);
2584
+ return SourceUtils._normalizeWorkspacePath(SourceUtils.makeWorkspacePath(segments), workspace, options);
2585
+ }
2586
+ return SourceUtils._normalizeWorkspacePath(SourceUtils._workspacePathPrefix + projectPath, workspace, options);
2587
+ }
2588
+ /**
2589
+ * Normalizes a workspace-relative path
2590
+ *
2591
+ * With an open workspace, the path's segments are collapsed and the `/`
2592
+ * prefix is kept. Without one, the path is normalized as an app-relative
2593
+ * path instead (see {@link SourceUtils._normalizeAppPath}), dropping its `/`
2594
+ * prefix: `/data/points.csv` then resolves like `//data/points.csv`.
2595
+ *
2596
+ * @param workspacePath - The workspace-relative path (with `/` prefix)
2597
+ * @param workspace - The directory handle of the open workspace, if any
2598
+ * @param options - Optional base URL for the app-relative fallback (see
2599
+ * {@link SourceUtils._normalizeAppPath})
2600
+ * @returns The normalized workspace-relative path, or the absolute URL if no
2601
+ * workspace is open
2602
+ * @throws Error if the path lacks the `/` prefix, or if it leaves the
2603
+ * workspace or names its root (with an open workspace) or cannot be
2604
+ * normalized as an app-relative path (without one, see
2605
+ * {@link SourceUtils._normalizeAppPath})
2606
+ */
2607
+ static _normalizeWorkspacePath(workspacePath, workspace, options) {
2608
+ if (!workspacePath.startsWith(SourceUtils._workspacePathPrefix)) throw new Error(`Invalid workspace-relative path: ${workspacePath}`);
2609
+ const path = workspacePath.substring(SourceUtils._workspacePathPrefix.length);
2610
+ if (workspace !== null) {
2611
+ const segments = SourceUtils._collapseSegments(path);
2612
+ if (segments.length === 0) throw new Error(`Not a workspace file or directory: ${workspacePath}`);
2613
+ return SourceUtils.makeWorkspacePath(segments);
2614
+ }
2615
+ return SourceUtils._normalizeAppPath(SourceUtils._appPathPrefix + path, options);
2616
+ }
2617
+ /**
2618
+ * Normalizes an app-relative path to an absolute URL
2619
+ *
2620
+ * The path is resolved against the base URL like a relative URL, so `..`
2621
+ * segments can lead above the application and a `/` right after the prefix
2622
+ * makes the path absolute within the origin.
2623
+ *
2624
+ * @param appPath - The app-relative path (with `//` prefix)
2625
+ * @param options - Optional base URL to resolve against (default
2626
+ * `document.baseURI`)
2627
+ * @returns The absolute URL
2628
+ * @throws Error if the path lacks the `//` prefix
2629
+ * @throws TypeError if the path does not form a valid URL with the base URL
2630
+ */
2631
+ static _normalizeAppPath(appPath, options) {
2632
+ const { baseUrl = document.baseURI } = options ?? {};
2633
+ if (!appPath.startsWith(SourceUtils._appPathPrefix)) throw new Error(`Invalid app-relative path: ${appPath}`);
2634
+ const path = appPath.substring(SourceUtils._appPathPrefix.length);
2635
+ return new URL(path, baseUrl).toString();
2636
+ }
2637
+ /**
2638
+ * Splits a path into segments, dropping empty and `.` segments and
2639
+ * collapsing `..` segments against the segments preceding them
2640
+ *
2641
+ * @param path - The path to split, using `/` as separator
2642
+ * @param baseSegments - The segments of the directory the path is relative
2643
+ * to, which `..` segments collapse against first (default: the root)
2644
+ * @returns The collapsed segments, empty if the path names the root itself
2645
+ * @throws Error if the path leads above the root
2646
+ */
2647
+ static _collapseSegments(path, baseSegments = []) {
2648
+ const segments = [...baseSegments];
2649
+ for (const segment of path.split(SourceUtils._pathSep)) {
2650
+ if (segment === "" || segment === ".") continue;
2651
+ if (segment === "..") {
2652
+ if (segments.length === 0) throw new Error(`Path escapes workspace: ${path}`);
2653
+ segments.pop();
2654
+ } else segments.push(segment);
2655
+ }
2656
+ return segments;
2657
+ }
2658
+ };
2659
+ //#endregion
2660
+ //#region src/utils/TableUtils.ts
2661
+ /**
2662
+ * Lookups into tabular data (`TableData`) by item ID
2663
+ *
2664
+ * A table annotates the items of another data object: each of its rows holds
2665
+ * the item ID it belongs to, and consumers address rows by that ID rather than
2666
+ * by position (see `ItemsData`). Looking rows up by ID needs a map from ID to
2667
+ * row index, which is linear in the size of the table to build and costly to
2668
+ * hold; {@link forEachRow} therefore builds it at most once per table, and not
2669
+ * at all in the common case that the IDs to look up are the table's own, in
2670
+ * row order. Once built, the map is retained for as long as the table's data
2671
+ * is: it is keyed by the identity of the table's ID array, so it is freed
2672
+ * together with the data, and a table that is looked up by other IDs - shapes
2673
+ * annotated by a separate table, or the items of one layer of an object -
2674
+ * keeps one entry per row alive until then. That is the price of sharing the
2675
+ * map between every consumer of the table.
2676
+ *
2677
+ * Item IDs are expected to be unique within a table (see `ItemsData`). For a
2678
+ * duplicated ID, the row-order fast path addresses each row by its own
2679
+ * position, whereas the lookup maps the ID to its last row; a table with
2680
+ * duplicated IDs is reported once, when its map is built.
2681
+ *
2682
+ * On top of the row lookup, {@link fillFromTableValues} and
2683
+ * {@link fillFromTableGroups} fill a typed array with one packed value per
2684
+ * item from a table column, one by parsing the cell values and one by mapping
2685
+ * their distinct values as groups. IDs that the table does not contain fall
2686
+ * back to a default value, with a warning.
2687
+ */
2688
+ var TableUtils = class TableUtils {
2689
+ /**
2690
+ * Row indices by item ID, by the table's ID array
2691
+ *
2692
+ * Keyed by the array that `getIds()` returns, which stays the very same array
2693
+ * for as long as the data is unchanged (see `ItemsData`), so an entry lives
2694
+ * exactly as long as the data it was built from. Holds the pending build,
2695
+ * so that concurrent lookups share it.
2696
+ */
2697
+ static _rowIndicesCache = /* @__PURE__ */ new WeakMap();
2698
+ /**
2699
+ * Fills `packedValues` by loading values from the given table column
2700
+ *
2701
+ * For each ID in `ids`, the corresponding row is looked up in the table by ID
2702
+ * (see {@link forEachRow}). The raw cell value is parsed by
2703
+ * `parseTableValue`; if that fails, or if the table does not contain the ID,
2704
+ * `defaultValue` is used instead.
2705
+ *
2706
+ * @param packedValues - Output typed array to fill, in the order of `ids`
2707
+ * @param tableData - The table to look up values in
2708
+ * @param ids - Ordered list of item IDs
2709
+ * @param column - Name of the table column to load values from
2710
+ * @param defaultValue - Value used when the ID is missing or parsing fails
2711
+ * @param parseTableValue - Converts a raw cell value to `TValue`, or to
2712
+ * `undefined` on failure
2713
+ * @param packValue - Converts `TValue` to the numeric representation stored
2714
+ * in `packedValues`
2715
+ * @param options - Optional abort signal
2716
+ */
2717
+ static async fillFromTableValues(packedValues, tableData, ids, column, defaultValue, parseTableValue, packValue, options) {
2718
+ const { signal } = options ?? {};
2719
+ signal?.throwIfAborted();
2720
+ const tableValues = await tableData.loadValues(column, { signal });
2721
+ let numMissingIds = 0;
2722
+ let numUnparsedValues = 0;
2723
+ await TableUtils.forEachRow(ids, tableData, (rowIndex, i) => {
2724
+ if (rowIndex !== void 0) {
2725
+ const parsedValue = parseTableValue(tableValues[rowIndex]);
2726
+ if (parsedValue === void 0) numUnparsedValues++;
2727
+ packedValues[i] = packValue(parsedValue ?? defaultValue);
2728
+ } else {
2729
+ numMissingIds++;
2730
+ packedValues[i] = packValue(defaultValue);
2731
+ }
2732
+ }, { signal });
2733
+ if (numMissingIds > 0) console.warn(`${numMissingIds} IDs missing in column ${column}, using default value`);
2734
+ if (numUnparsedValues > 0) console.warn(`Failed to parse ${numUnparsedValues} values from column ${column}, using default value`);
2735
+ }
2736
+ /**
2737
+ * Fills `packedValues` by grouping IDs by the given table column and mapping the groups to values
2738
+ *
2739
+ * For each ID in `ids`, the corresponding row is looked up in the table by ID
2740
+ * (see {@link forEachRow}). The raw cell value is converted to a string to
2741
+ * form the group key, which `mapGroupToValue` maps to a value; if the table
2742
+ * does not contain the ID, `defaultValue` is used instead. Groups are
2743
+ * resolved once per distinct cell value, i.e. `mapGroupToValue` is not
2744
+ * called per item.
2745
+ *
2746
+ * @param packedValues - Output typed array to fill, in the order of `ids`
2747
+ * @param tableData - The table to look up group keys in
2748
+ * @param ids - Ordered list of item IDs
2749
+ * @param column - Name of the table column to load group keys from
2750
+ * @param defaultValue - Value used when the ID is missing
2751
+ * @param mapGroupToValue - Maps a group key (the cell value as a string) to
2752
+ * `TValue`
2753
+ * @param packValue - Converts `TValue` to the numeric representation stored
2754
+ * in `packedValues`
2755
+ * @param options - Optional abort signal
2756
+ */
2757
+ static async fillFromTableGroups(packedValues, tableData, ids, column, defaultValue, mapGroupToValue, packValue, options) {
2758
+ const { signal } = options ?? {};
2759
+ signal?.throwIfAborted();
2760
+ const tableGroups = await tableData.loadValues(column, { signal });
2761
+ let numMissingIds = 0;
2762
+ const packedValueByTableGroup = /* @__PURE__ */ new Map();
2763
+ await TableUtils.forEachRow(ids, tableData, (rowIndex, i) => {
2764
+ if (rowIndex !== void 0) {
2765
+ const tableGroup = tableGroups[rowIndex];
2766
+ let packedValue = packedValueByTableGroup.get(tableGroup);
2767
+ if (packedValue === void 0) {
2768
+ packedValue = packValue(mapGroupToValue(String(tableGroup)));
2769
+ packedValueByTableGroup.set(tableGroup, packedValue);
2770
+ }
2771
+ packedValues[i] = packedValue;
2772
+ } else {
2773
+ numMissingIds++;
2774
+ packedValues[i] = packValue(defaultValue);
2775
+ }
2776
+ }, { signal });
2777
+ if (numMissingIds > 0) console.warn(`${numMissingIds} IDs missing in column ${column}, using default value`);
2778
+ }
2779
+ /**
2780
+ * Calls `callback` for every ID with the index of the table row holding that ID
2781
+ *
2782
+ * If `ids` is the table's own ID array, the ID at position `i` is held by
2783
+ * row `i` and no lookup is needed. Otherwise the rows are looked up in the
2784
+ * table's row indices (see {@link getRowIndices}); IDs the table does not
2785
+ * contain are reported with an undefined row index.
2786
+ *
2787
+ * @param ids - Ordered list of item IDs
2788
+ * @param tableData - The table to look the IDs up in
2789
+ * @param callback - Called with the row index of each ID, or `undefined` if
2790
+ * the table does not contain it, and the position of the ID in `ids`
2791
+ * @param options - Optional abort signal
2792
+ */
2793
+ static async forEachRow(ids, tableData, callback, options) {
2794
+ const { signal } = options ?? {};
2795
+ signal?.throwIfAborted();
2796
+ if (ids === tableData.getIds()) {
2797
+ await AsyncUtils.forEach(ids, (_, i) => callback(i, i), { signal });
2798
+ return;
2799
+ }
2800
+ const rowIndices = await TableUtils.getRowIndices(tableData, { signal });
2801
+ await AsyncUtils.forEach(ids, (id, i) => callback(rowIndices.get(id), i), { signal });
2802
+ }
2803
+ /**
2804
+ * Returns the row indices of a table by item ID
2805
+ *
2806
+ * Built at most once per table, cooperatively yielding, and shared by all
2807
+ * callers, which is why a caller's abort signal only detaches that caller:
2808
+ * the build itself keeps going for the others, and for the next lookup.
2809
+ *
2810
+ * @param tableData - The table to return the row indices of
2811
+ * @param options - Optional abort signal
2812
+ * @returns The row index of every item ID of the table; for an ID occurring
2813
+ * more than once, which is reported with a warning, the index of its last row
2814
+ */
2815
+ static getRowIndices(tableData, options) {
2816
+ const tableIds = tableData.getIds();
2817
+ let rowIndicesPromise = TableUtils._rowIndicesCache.get(tableIds);
2818
+ if (rowIndicesPromise === void 0) {
2819
+ rowIndicesPromise = TableUtils._buildRowIndices(tableIds);
2820
+ TableUtils._rowIndicesCache.set(tableIds, rowIndicesPromise);
2821
+ }
2822
+ return AsyncUtils.raceSignal(rowIndicesPromise, options);
2823
+ }
2824
+ /**
2825
+ * Builds the row indices by item ID for the given table IDs
2826
+ */
2827
+ static async _buildRowIndices(tableIds) {
2828
+ const rowIndices = /* @__PURE__ */ new Map();
2829
+ await AsyncUtils.forEach(tableIds, (id, i) => {
2830
+ rowIndices.set(id, i);
2831
+ });
2832
+ if (rowIndices.size !== tableIds.length) console.warn(`${tableIds.length - rowIndices.size} duplicated item IDs in table, using the last row of each`);
2833
+ return rowIndices;
2834
+ }
2835
+ };
2836
+ //#endregion
2837
+ //#region src/utils/TransformUtils.ts
2838
+ /**
2839
+ * Utility methods for converting between {@link SimilarityTransform}
2840
+ * objects and `gl-matrix` {@link mat3} matrices
2841
+ */
2842
+ var TransformUtils = class {
2843
+ /**
2844
+ * Decomposes a 3×3 similarity matrix into a {@link SimilarityTransform}
2845
+ *
2846
+ * Extracts flip, uniform scale, rotation (in degrees), and translation
2847
+ * from a column-major `gl-matrix` {@link mat3}. A negative 2D determinant
2848
+ * indicates a horizontal reflection.
2849
+ *
2850
+ * If `pivot` is given, flip and rotation are expressed about `pivot`
2851
+ * (in the input coordinates of `m`) instead of the origin, while scale
2852
+ * stays about the origin. Flip, scale, and rotation are unaffected; only
2853
+ * the translation changes to `m(pivot) - scale * pivot`, i.e. the position
2854
+ * of the scaled content before it is flipped and rotated about its (scaled)
2855
+ * pivot. This matches viewers that place an image by its top-left corner
2856
+ * and then flip/rotate it about its center.
2857
+ *
2858
+ * @param m - The source matrix
2859
+ * @param pivot - Optional point about which flip and rotation are expressed
2860
+ * @returns The decomposed transform
2861
+ */
2862
+ static decompose(m, pivot) {
2863
+ const flip = m[0] * m[4] - m[3] * m[1] < 0;
2864
+ const c0 = flip ? -m[0] : m[0];
2865
+ const c1 = flip ? -m[1] : m[1];
2866
+ const scale = Math.hypot(c0, c1);
2867
+ const rotation = Math.atan2(c1, c0) * 180 / Math.PI;
2868
+ const translation = {
2869
+ x: m[6],
2870
+ y: m[7]
2871
+ };
2872
+ if (pivot !== void 0) {
2873
+ const { x: cx, y: cy } = pivot;
2874
+ translation.x = m[0] * cx + m[3] * cy + m[6] - scale * cx;
2875
+ translation.y = m[1] * cx + m[4] * cy + m[7] - scale * cy;
2876
+ }
2877
+ return {
2878
+ flip,
2879
+ scale,
2880
+ rotation,
2881
+ translation
2882
+ };
2883
+ }
2884
+ /**
2885
+ * Builds a 3×3 similarity matrix from a (partial) {@link SimilarityTransform}
2886
+ *
2887
+ * Applies, in order: flip/scale, rotation, and translation.
2888
+ *
2889
+ * If `pivot` is given, flip and rotation are applied about `pivot` (in the
2890
+ * source coordinates of the transform) instead of the origin, while scale
2891
+ * stays about the origin. The translation is then the position of the
2892
+ * scaled content before it is flipped and rotated about its (scaled)
2893
+ * pivot. This is the inverse of {@link decompose} called with
2894
+ * the same pivot.
2895
+ *
2896
+ * @param tf - The transform components (all optional)
2897
+ * @param pivot - Optional point about which flip and rotation are applied
2898
+ * @returns The composed matrix
2899
+ */
2900
+ static compose(tf, pivot) {
2901
+ const m = mat3.create();
2902
+ if (tf.translation !== void 0) mat3.translate(m, m, [tf.translation.x, tf.translation.y]);
2903
+ if (pivot !== void 0) {
2904
+ const scale = tf.scale ?? 1;
2905
+ mat3.translate(m, m, [scale * pivot.x, scale * pivot.y]);
2906
+ }
2907
+ if (tf.rotation !== void 0) mat3.rotate(m, m, Math.PI * tf.rotation / 180);
2908
+ if (tf.flip) mat3.scale(m, m, [-1, 1]);
2909
+ if (pivot !== void 0) {
2910
+ const scale = tf.scale ?? 1;
2911
+ mat3.translate(m, m, [-scale * pivot.x, -scale * pivot.y]);
2912
+ }
2913
+ if (tf.scale !== void 0) mat3.scale(m, m, [tf.scale, tf.scale]);
2914
+ return m;
2915
+ }
2916
+ /**
2917
+ * Transforms an axis-aligned rectangle and returns the axis-aligned bounding box of the transformed corners
2918
+ *
2919
+ * @param rect - The rectangle to transform
2920
+ * @param m - The transformation matrix to apply to the rectangle corners
2921
+ * @returns The axis-aligned bounding box of the transformed rectangle
2922
+ */
2923
+ static transformBoundingBox(rect, m) {
2924
+ const { x, y, width, height } = rect;
2925
+ const p0 = vec2.fromValues(x, y);
2926
+ const p1 = vec2.fromValues(x + width, y);
2927
+ const p2 = vec2.fromValues(x, y + height);
2928
+ const p3 = vec2.fromValues(x + width, y + height);
2929
+ vec2.transformMat3(p0, p0, m);
2930
+ vec2.transformMat3(p1, p1, m);
2931
+ vec2.transformMat3(p2, p2, m);
2932
+ vec2.transformMat3(p3, p3, m);
2933
+ const xMin = Math.min(p0[0], p1[0], p2[0], p3[0]);
2934
+ const yMin = Math.min(p0[1], p1[1], p2[1], p3[1]);
2935
+ const xMax = Math.max(p0[0], p1[0], p2[0], p3[0]);
2936
+ const yMax = Math.max(p0[1], p1[1], p2[1], p3[1]);
2937
+ return {
2938
+ x: xMin,
2939
+ y: yMin,
2940
+ width: xMax - xMin,
2941
+ height: yMax - yMin
2942
+ };
2943
+ }
2944
+ };
2945
+ //#endregion
2946
+ //#region src/assets/palettes/batlow.txt?raw
2947
+ var batlow_default = "0.005193 0.098238 0.349842\n0.009065 0.104487 0.350933\n0.012963 0.110779 0.351992\n0.016530 0.116913 0.353070\n0.019936 0.122985 0.354120\n0.023189 0.129035 0.355182\n0.026291 0.135044 0.356210\n0.029245 0.140964 0.357239\n0.032053 0.146774 0.358239\n0.034853 0.152558 0.359233\n0.037449 0.158313 0.360216\n0.039845 0.163978 0.361187\n0.042104 0.169557 0.362151\n0.044069 0.175053 0.363084\n0.045905 0.180460 0.364007\n0.047665 0.185844 0.364915\n0.049378 0.191076 0.365810\n0.050795 0.196274 0.366684\n0.052164 0.201323 0.367524\n0.053471 0.206357 0.368370\n0.054721 0.211234 0.369184\n0.055928 0.216046 0.369974\n0.057033 0.220754 0.370750\n0.058032 0.225340 0.371509\n0.059164 0.229842 0.372252\n0.060167 0.234299 0.372978\n0.061052 0.238625 0.373691\n0.062060 0.242888 0.374386\n0.063071 0.247085 0.375050\n0.063982 0.251213 0.375709\n0.064936 0.255264 0.376362\n0.065903 0.259257 0.376987\n0.066899 0.263188 0.377594\n0.067921 0.267056 0.378191\n0.069002 0.270922 0.378774\n0.070001 0.274713 0.379342\n0.071115 0.278497 0.379895\n0.072192 0.282249 0.380434\n0.073440 0.285942 0.380957\n0.074595 0.289653 0.381452\n0.075833 0.293321 0.381922\n0.077136 0.296996 0.382376\n0.078517 0.300622 0.382814\n0.079984 0.304252 0.383224\n0.081553 0.307858 0.383598\n0.083082 0.311461 0.383936\n0.084778 0.315043 0.384240\n0.086503 0.318615 0.384506\n0.088353 0.322167 0.384731\n0.090281 0.325685 0.384910\n0.092304 0.329220 0.385040\n0.094462 0.332712 0.385116\n0.096618 0.336161 0.385134\n0.099015 0.339621 0.385090\n0.101481 0.343036 0.384981\n0.104078 0.346410 0.384801\n0.106842 0.349774 0.384548\n0.109695 0.353098 0.384217\n0.112655 0.356391 0.383807\n0.115748 0.359638 0.383310\n0.118992 0.362849 0.382713\n0.122320 0.366030 0.382026\n0.125889 0.369160 0.381259\n0.129519 0.372238 0.380378\n0.133298 0.375282 0.379395\n0.137212 0.378282 0.378315\n0.141260 0.381240 0.377135\n0.145432 0.384130 0.375840\n0.149706 0.386975 0.374449\n0.154073 0.389777 0.372934\n0.158620 0.392531 0.371320\n0.163246 0.395237 0.369609\n0.167952 0.397889 0.367784\n0.172788 0.400496 0.365867\n0.177752 0.403041 0.363833\n0.182732 0.405551 0.361714\n0.187886 0.408003 0.359484\n0.193050 0.410427 0.357177\n0.198310 0.412798 0.354767\n0.203676 0.415116 0.352253\n0.209075 0.417412 0.349677\n0.214555 0.419661 0.347019\n0.220112 0.421864 0.344261\n0.225707 0.424049 0.341459\n0.231362 0.426197 0.338572\n0.237075 0.428325 0.335634\n0.242795 0.430418 0.332635\n0.248617 0.432493 0.329571\n0.254452 0.434529 0.326434\n0.260320 0.436556 0.323285\n0.266241 0.438555 0.320085\n0.272168 0.440541 0.316831\n0.278171 0.442524 0.313552\n0.284175 0.444484 0.310243\n0.290214 0.446420 0.306889\n0.296294 0.448357 0.303509\n0.302379 0.450282 0.300122\n0.308517 0.452205 0.296721\n0.314648 0.454107 0.293279\n0.320834 0.456006 0.289841\n0.327007 0.457900 0.286377\n0.333235 0.459794 0.282937\n0.339469 0.461685 0.279468\n0.345703 0.463563 0.275998\n0.351976 0.465440 0.272492\n0.358277 0.467331 0.269037\n0.364589 0.469213 0.265543\n0.370922 0.471085 0.262064\n0.377291 0.472952 0.258588\n0.383675 0.474842 0.255131\n0.390070 0.476711 0.251665\n0.396505 0.478587 0.248212\n0.402968 0.480466 0.244731\n0.409455 0.482351 0.241314\n0.415967 0.484225 0.237895\n0.422507 0.486113 0.234493\n0.429094 0.488011 0.231096\n0.435714 0.489890 0.227728\n0.442365 0.491795 0.224354\n0.449052 0.493684 0.221074\n0.455774 0.495585 0.217774\n0.462539 0.497497 0.214518\n0.469368 0.499393 0.211318\n0.476221 0.501314 0.208148\n0.483123 0.503216 0.205037\n0.490081 0.505137 0.201976\n0.497089 0.507058 0.198994\n0.504153 0.508984 0.196118\n0.511253 0.510898 0.193296\n0.518425 0.512822 0.190566\n0.525637 0.514746 0.187990\n0.532907 0.516662 0.185497\n0.540225 0.518584 0.183099\n0.547599 0.520486 0.180884\n0.555024 0.522391 0.178854\n0.562506 0.524293 0.176964\n0.570016 0.526186 0.175273\n0.577582 0.528058 0.173775\n0.585199 0.529927 0.172493\n0.592846 0.531777 0.171449\n0.600520 0.533605 0.170648\n0.608240 0.535423 0.170104\n0.615972 0.537231 0.169826\n0.623739 0.539002 0.169814\n0.631513 0.540752 0.170075\n0.639301 0.542484 0.170622\n0.647098 0.544183 0.171465\n0.654889 0.545863 0.172603\n0.662691 0.547503 0.174044\n0.670477 0.549127 0.175747\n0.678244 0.550712 0.177803\n0.685995 0.552274 0.180056\n0.693720 0.553797 0.182610\n0.701421 0.555294 0.185478\n0.709098 0.556772 0.188546\n0.716731 0.558205 0.191851\n0.724322 0.559628 0.195408\n0.731878 0.561011 0.199174\n0.739393 0.562386 0.203179\n0.746850 0.563725 0.207375\n0.754268 0.565033 0.211761\n0.761629 0.566344 0.216322\n0.768942 0.567630 0.221045\n0.776208 0.568899 0.225930\n0.783416 0.570162 0.230962\n0.790568 0.571421 0.236160\n0.797665 0.572682 0.241490\n0.804709 0.573928 0.246955\n0.811692 0.575187 0.252572\n0.818610 0.576462 0.258303\n0.825472 0.577725 0.264197\n0.832272 0.579026 0.270211\n0.838999 0.580339 0.276353\n0.845657 0.581672 0.282631\n0.852247 0.583037 0.289036\n0.858747 0.584440 0.295572\n0.865168 0.585882 0.302255\n0.871505 0.587352 0.309112\n0.877741 0.588873 0.316081\n0.883878 0.590450 0.323195\n0.889900 0.592087 0.330454\n0.895809 0.593765 0.337865\n0.901590 0.595507 0.345429\n0.907242 0.597319 0.353142\n0.912746 0.599191 0.360986\n0.918103 0.601126 0.368999\n0.923300 0.603137 0.377139\n0.928323 0.605212 0.385404\n0.933176 0.607369 0.393817\n0.937850 0.609582 0.402345\n0.942332 0.611867 0.411006\n0.946612 0.614218 0.419767\n0.950697 0.616649 0.428624\n0.954574 0.619137 0.437582\n0.958244 0.621671 0.446604\n0.961696 0.624282 0.455702\n0.964943 0.626934 0.464860\n0.967983 0.629639 0.474057\n0.970804 0.632394 0.483290\n0.973424 0.635183 0.492547\n0.975835 0.638012 0.501826\n0.978052 0.640868 0.511090\n0.980079 0.643752 0.520350\n0.981918 0.646664 0.529602\n0.983574 0.649590 0.538819\n0.985066 0.652522 0.547998\n0.986392 0.655470 0.557142\n0.987567 0.658422 0.566226\n0.988596 0.661378 0.575265\n0.989496 0.664329 0.584246\n0.990268 0.667280 0.593174\n0.990926 0.670230 0.602031\n0.991479 0.673165 0.610835\n0.991935 0.676091 0.619575\n0.992305 0.679007 0.628251\n0.992595 0.681914 0.636869\n0.992813 0.684815 0.645423\n0.992967 0.687705 0.653934\n0.993064 0.690579 0.662398\n0.993111 0.693451 0.670810\n0.993112 0.696314 0.679177\n0.993074 0.699161 0.687519\n0.993002 0.702006 0.695831\n0.992900 0.704852 0.704114\n0.992771 0.707689 0.712380\n0.992619 0.710530 0.720639\n0.992447 0.713366 0.728892\n0.992258 0.716210 0.737146\n0.992054 0.719049 0.745403\n0.991837 0.721893 0.753673\n0.991607 0.724754 0.761959\n0.991367 0.727614 0.770270\n0.991116 0.730489 0.778606\n0.990855 0.733373 0.786976\n0.990586 0.736265 0.795371\n0.990307 0.739184 0.803810\n0.990018 0.742102 0.812285\n0.989720 0.745039 0.820804\n0.989411 0.747997 0.829372\n0.989089 0.750968 0.837979\n0.988754 0.753949 0.846627\n0.988406 0.756949 0.855332\n0.988046 0.759964 0.864078\n0.987672 0.762996 0.872864\n0.987280 0.766047 0.881699\n0.986868 0.769105 0.890573\n0.986435 0.772184 0.899493\n0.985980 0.775272 0.908448\n0.985503 0.778378 0.917444\n0.985002 0.781495 0.926468\n0.984473 0.784624 0.935531\n0.983913 0.787757 0.944626\n0.983322 0.790905 0.953748\n0.982703 0.794068 0.962895\n0.982048 0.797228 0.972070\n0.981354 0.800406 0.981267\n";
2948
+ //#endregion
2949
+ //#region src/assets/palettes/batlowS.txt?raw
2950
+ var batlowS_default = "0.005193 0.098238 0.349842\n0.981354 0.800406 0.981267\n0.511253 0.510898 0.193296\n0.133298 0.375282 0.379395\n0.946612 0.614218 0.419767\n0.066899 0.263188 0.377594\n0.992900 0.704852 0.704114\n0.302379 0.450282 0.300122\n0.754268 0.565033 0.211761\n0.402968 0.480466 0.244731\n0.989089 0.750968 0.837979\n0.631513 0.540752 0.170075\n0.987567 0.658422 0.566226\n0.209075 0.417412 0.349677\n0.049378 0.191076 0.365810\n0.088353 0.322167 0.384731\n0.858747 0.584440 0.295572\n0.973424 0.635183 0.492547\n0.167952 0.397889 0.367784\n0.570016 0.526186 0.175273\n0.351976 0.465440 0.272492\n0.106842 0.349774 0.384548\n0.991367 0.727614 0.770270\n0.693720 0.553797 0.182610\n0.032053 0.146774 0.358239\n0.075833 0.293321 0.381922\n0.254452 0.434529 0.326434\n0.907242 0.597319 0.353142\n0.059164 0.229842 0.372252\n0.985980 0.775272 0.908448\n0.992595 0.681914 0.636869\n0.455774 0.495585 0.217774\n0.811692 0.575187 0.252572\n0.662691 0.547503 0.174044\n0.987672 0.762996 0.872864\n0.187886 0.408003 0.359484\n0.992258 0.716210 0.737146\n0.019936 0.122985 0.354120\n0.429094 0.488011 0.231096\n0.071115 0.278497 0.379895\n0.231362 0.426197 0.338572\n0.081553 0.307858 0.383598\n0.783416 0.570162 0.230962\n0.883878 0.590450 0.323195\n0.278171 0.442524 0.313552\n0.054721 0.211234 0.369184\n0.042104 0.169557 0.362151\n0.063071 0.247085 0.375050\n0.118992 0.362849 0.382713\n0.096618 0.336161 0.385134\n0.983913 0.787757 0.944626\n0.981918 0.646664 0.529602\n0.990926 0.670230 0.602031\n0.149706 0.386975 0.374449\n0.377291 0.472952 0.258588\n0.540225 0.518584 0.183099\n0.990307 0.739184 0.803810\n0.993111 0.693451 0.670810\n0.961696 0.624282 0.455702\n0.483123 0.503216 0.205037\n0.600520 0.533605 0.170648\n0.928323 0.605212 0.385404\n0.327007 0.457900 0.286377\n0.724322 0.559628 0.195408\n0.832272 0.579026 0.270211\n0.978052 0.640868 0.511090\n0.993074 0.699161 0.687519\n0.954574 0.619137 0.437582\n0.364589 0.469213 0.265543\n0.220112 0.421864 0.344261\n0.045905 0.180460 0.364007\n0.989496 0.664329 0.584246\n0.061052 0.238625 0.373691\n0.012963 0.110779 0.351992\n0.158620 0.392531 0.371320\n0.988406 0.756949 0.855332\n0.797665 0.572682 0.241490\n0.314648 0.454107 0.293279\n0.992967 0.687705 0.653934\n0.768942 0.567630 0.221045\n0.647098 0.544183 0.171465\n0.390070 0.476711 0.251665\n0.985002 0.781495 0.926468\n0.985066 0.652522 0.547998\n0.026291 0.135044 0.356210\n0.078517 0.300622 0.382814\n0.469368 0.499393 0.211318\n0.585199 0.529927 0.172493\n0.141260 0.381240 0.377135\n0.415967 0.484225 0.237895\n0.290214 0.446420 0.306889\n0.073440 0.285942 0.380957\n0.986868 0.769105 0.890573\n0.242795 0.430418 0.332635\n0.991935 0.676091 0.619575\n0.052164 0.201323 0.367524\n0.442365 0.491795 0.224354\n0.064936 0.255264 0.376362\n0.198310 0.412798 0.354767\n0.989720 0.745039 0.820804\n";
2951
+ //#endregion
2952
+ //#region src/assets/palettes/distinct.txt?raw
2953
+ var distinct_default = "230 25 75\n60 180 75\n255 225 25\n67 99 216\n245 130 49\n145 30 180\n66 212 244\n240 50 230\n191 239 69\n250 190 212\n70 153 144\n220 190 255\n154 99 36\n255 250 200\n128 0 0\n170 255 195\n128 128 0\n255 216 177\n0 0 117\n169 169 169\n";
2954
+ //#endregion
2955
+ //#region src/palettes.ts
2956
+ /** Ordered palette of marker shapes, for distinguishing categorical groups */
2957
+ var markerPalette = [
2958
+ Marker.Cross,
2959
+ Marker.Diamond,
2960
+ Marker.Square,
2961
+ Marker.TriangleUp,
2962
+ Marker.Star,
2963
+ Marker.Clobber,
2964
+ Marker.Disc,
2965
+ Marker.HBar,
2966
+ Marker.VBar,
2967
+ Marker.TailedArrow,
2968
+ Marker.TriangleDown,
2969
+ Marker.Ring,
2970
+ Marker.X,
2971
+ Marker.Arrow,
2972
+ Marker.Gaussian
2973
+ ];
2974
+ /** Number of colors sampled from each continuous D3 color scheme */
2975
+ var sampledColorPaletteSize = 256;
2976
+ /**
2977
+ * Builds a color palette by sampling a continuous D3 color scheme
2978
+ *
2979
+ * @param id - Color palette ID, persisted in project configs
2980
+ * @param name - Human-readable color palette name
2981
+ * @param colorScheme - Maps a position within `[0, 1]` to a CSS color string
2982
+ * @returns The color palette, holding `sampledColorPaletteSize` colors
2983
+ */
2984
+ /* @__NO_SIDE_EFFECTS__ */
2985
+ function sampledColorPalette(id, name, colorScheme) {
2986
+ return {
2987
+ id,
2988
+ name,
2989
+ colors: ColorUtils.sampleColorScheme(colorScheme, sampledColorPaletteSize)
2990
+ };
2991
+ }
2992
+ /**
2993
+ * Builds a color palette from a list of hex colors
2994
+ *
2995
+ * @param id - Color palette ID, persisted in project configs
2996
+ * @param name - Human-readable color palette name
2997
+ * @param hexColors - The colors making up the palette, in order
2998
+ * @returns The color palette
2999
+ */
3000
+ /* @__NO_SIDE_EFFECTS__ */
3001
+ function hexColorPalette(id, name, hexColors) {
3002
+ return {
3003
+ id,
3004
+ name,
3005
+ colors: hexColors.map((hex) => ColorUtils.fromHex(hex))
3006
+ };
3007
+ }
3008
+ /** Color palettes suitable for continuous data */
3009
+ var continuousColorPalettes = [
3010
+ /* @__PURE__ */ sampledColorPalette("viridis", "Viridis", interpolateViridis),
3011
+ /* @__PURE__ */ sampledColorPalette("magma", "Magma", interpolateMagma),
3012
+ /* @__PURE__ */ sampledColorPalette("inferno", "Inferno", interpolateInferno),
3013
+ /* @__PURE__ */ sampledColorPalette("plasma", "Plasma", interpolatePlasma),
3014
+ /* @__PURE__ */ sampledColorPalette("cividis", "Cividis", interpolateCividis),
3015
+ /* @__PURE__ */ sampledColorPalette("turbo", "Turbo", interpolateTurbo),
3016
+ /* @__PURE__ */ sampledColorPalette("blues", "Blues", interpolateBlues),
3017
+ /* @__PURE__ */ sampledColorPalette("brbg", "BrBG", interpolateBrBG),
3018
+ /* @__PURE__ */ sampledColorPalette("bugn", "BuGn", interpolateBuGn),
3019
+ /* @__PURE__ */ sampledColorPalette("bupu", "BuPu", interpolateBuPu),
3020
+ /* @__PURE__ */ sampledColorPalette("cool", "Cool", interpolateCool),
3021
+ /* @__PURE__ */ sampledColorPalette("cubehelix", "Cubehelix", interpolateCubehelixDefault),
3022
+ /* @__PURE__ */ sampledColorPalette("gnbu", "GnBu", interpolateGnBu),
3023
+ /* @__PURE__ */ sampledColorPalette("greens", "Greens", interpolateGreens),
3024
+ /* @__PURE__ */ sampledColorPalette("greys", "Greys", interpolateGreys),
3025
+ /* @__PURE__ */ sampledColorPalette("oranges", "Oranges", interpolateOranges),
3026
+ /* @__PURE__ */ sampledColorPalette("orrd", "OrRd", interpolateOrRd),
3027
+ /* @__PURE__ */ sampledColorPalette("piyg", "PiYG", interpolatePiYG),
3028
+ /* @__PURE__ */ sampledColorPalette("prgn", "PRGn", interpolatePRGn),
3029
+ /* @__PURE__ */ sampledColorPalette("pubu", "PuBu", interpolatePuBu),
3030
+ /* @__PURE__ */ sampledColorPalette("pubugn", "PuBuGn", interpolatePuBuGn),
3031
+ /* @__PURE__ */ sampledColorPalette("puor", "PuOr", interpolatePuOr),
3032
+ /* @__PURE__ */ sampledColorPalette("purd", "PuRd", interpolatePuRd),
3033
+ /* @__PURE__ */ sampledColorPalette("purples", "Purples", interpolatePurples),
3034
+ /* @__PURE__ */ sampledColorPalette("rainbow", "Rainbow", interpolateRainbow),
3035
+ /* @__PURE__ */ sampledColorPalette("rdbu", "RdBu", interpolateRdBu),
3036
+ /* @__PURE__ */ sampledColorPalette("rdgy", "RdGy", interpolateRdGy),
3037
+ /* @__PURE__ */ sampledColorPalette("rdpu", "RdPu", interpolateRdPu),
3038
+ /* @__PURE__ */ sampledColorPalette("rdylbu", "RdYlBu", interpolateRdYlBu),
3039
+ /* @__PURE__ */ sampledColorPalette("rdylgn", "RdYlGn", interpolateRdYlGn),
3040
+ /* @__PURE__ */ sampledColorPalette("reds", "Reds", interpolateReds),
3041
+ /* @__PURE__ */ sampledColorPalette("sinebow", "Sinebow", interpolateSinebow),
3042
+ /* @__PURE__ */ sampledColorPalette("spectral", "Spectral", interpolateSpectral),
3043
+ /* @__PURE__ */ sampledColorPalette("warm", "Warm", interpolateWarm),
3044
+ /* @__PURE__ */ sampledColorPalette("ylgn", "YlGn", interpolateYlGn),
3045
+ /* @__PURE__ */ sampledColorPalette("ylgnbu", "YlGnBu", interpolateYlGnBu),
3046
+ /* @__PURE__ */ sampledColorPalette("ylorbr", "YlOrBr", interpolateYlOrBr),
3047
+ /* @__PURE__ */ sampledColorPalette("ylorrd", "YlOrRd", interpolateYlOrRd),
3048
+ {
3049
+ id: "batlow",
3050
+ name: "Batlow",
3051
+ colors: /* @__PURE__ */ ColorUtils.parsePalette(batlow_default)
3052
+ }
3053
+ ];
3054
+ /** Color palettes suitable for categorical data */
3055
+ var categoricalColorPalettes = [
3056
+ {
3057
+ id: "distinct",
3058
+ name: "Distinct",
3059
+ colors: /* @__PURE__ */ ColorUtils.parsePalette(distinct_default, { maxValue: 255 })
3060
+ },
3061
+ /* @__PURE__ */ hexColorPalette("category10", "Category 10", schemeCategory10),
3062
+ /* @__PURE__ */ hexColorPalette("observable10", "Observable 10", schemeObservable10),
3063
+ /* @__PURE__ */ hexColorPalette("tableau10", "Tableau 10", schemeTableau10),
3064
+ /* @__PURE__ */ hexColorPalette("accent", "Accent", schemeAccent),
3065
+ /* @__PURE__ */ hexColorPalette("dark2", "Dark 2", schemeDark2),
3066
+ /* @__PURE__ */ hexColorPalette("paired", "Paired", schemePaired),
3067
+ /* @__PURE__ */ hexColorPalette("pastel1", "Pastel 1", schemePastel1),
3068
+ /* @__PURE__ */ hexColorPalette("pastel2", "Pastel 2", schemePastel2),
3069
+ /* @__PURE__ */ hexColorPalette("set1", "Set 1", schemeSet1),
3070
+ /* @__PURE__ */ hexColorPalette("set2", "Set 2", schemeSet2),
3071
+ /* @__PURE__ */ hexColorPalette("set3", "Set 3", schemeSet3),
3072
+ {
3073
+ id: "batlowS",
3074
+ name: "Batlow",
3075
+ colors: /* @__PURE__ */ ColorUtils.parsePalette(batlowS_default)
3076
+ }
3077
+ ];
3078
+ /** All available color palettes, continuous and categorical */
3079
+ var colorPalettes = /* @__PURE__ */ continuousColorPalettes.concat(categoricalColorPalettes);
3080
+ /**
3081
+ * Looks up a color palette by ID
3082
+ *
3083
+ * @param colorPaletteId - The ID of the color palette
3084
+ * @returns The color palette, or `undefined` if no palette has the ID
3085
+ */
3086
+ /* @__NO_SIDE_EFFECTS__ */
3087
+ function findColorPalette(colorPaletteId) {
3088
+ return colorPalettes.find((colorPalette) => colorPalette.id === colorPaletteId);
3089
+ }
3090
+ //#endregion
3091
+ export { ArrayUtils, AsyncUtils, BitUtils, ColorUtils, ConfigUtils, GeometryUtils, HashUtils, ImageChannelViewMode, ImageUtils, JSONUtils, Marker, MathUtils, NumberUtils, ProjectUtils, RandomUtils, SourceUtils, TableUtils, TransformUtils, annotatedDataSourceDefaults, categoricalColorPalettes, colorPalettes, continuousColorPalettes, createAnnotatedDataSource, createDataObject, createDataSource, createImage, createImageDataSource, createLabels, createLabelsDataSource, createLayer, createModel, createPoints, createPointsDataSource, createProject, createRenderedDataObject, createRenderedItemsDataObject, createRenderedRasterDataObject, createShapes, createShapesDataSource, createTable, createTableDataSource, dataObjectDefaults, dataSourceDefaults, defaultLabelColor, defaultLabelColorPalette, defaultLabelOpacity, defaultLabelVisibility, defaultPointColor, defaultPointMarker, defaultPointOpacity, defaultPointSize, defaultPointSizeUnit, defaultPointVisibility, defaultRandomSeed, defaultShapeFillColor, defaultShapeFillOpacity, defaultShapeFillVisibility, defaultShapeOpacity, defaultShapeStrokeColor, defaultShapeStrokeOpacity, defaultShapeStrokeVisibility, defaultShapeVisibility, findColorPalette, getActiveConfigSource, identityTransform, imageDataSourceDefaults, imageDefaults, isConstantConfig, isFromConfig, isGroupByConfig, isRandomConfig, labelsDataSourceDefaults, labelsDefaults, layerDefaults, markerPalette, modelDefaults, pointsDataSourceDefaults, pointsDefaults, projectDefaults, renderedDataObjectDefaults, renderedItemsDataObjectDefaults, renderedRasterDataObjectDefaults, shapesDataSourceDefaults, shapesDefaults, tableDataSourceDefaults, tableDefaults };