@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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +24 -0
- package/dist/index.d.ts +4533 -0
- package/dist/index.js +3091 -0
- package/package.json +62 -0
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 };
|