cloudmr-ux 4.3.7 → 4.3.8

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.
Files changed (49) hide show
  1. package/dist/CmrComponents/niivue-contrast-adjustments/NiivueContrastAdjustments.js +4 -4
  2. package/dist/CmrComponents/niivue-slice-position/NiivueSlicePosition.js +2 -2
  3. package/dist/CmrComponents/niivue-viewer/CloudMrNiivuePanel.d.ts +38 -0
  4. package/dist/CmrComponents/niivue-viewer/CloudMrNiivuePanel.js +197 -0
  5. package/dist/CmrComponents/niivue-viewer/CloudMrNiivueViewer.d.ts +41 -0
  6. package/dist/CmrComponents/niivue-viewer/CloudMrNiivueViewer.js +1239 -0
  7. package/dist/CmrComponents/niivue-viewer/ColorPicker.d.ts +1 -0
  8. package/dist/CmrComponents/niivue-viewer/ColorPicker.js +65 -0
  9. package/dist/CmrComponents/niivue-viewer/Layer.d.ts +1 -0
  10. package/dist/CmrComponents/niivue-viewer/Layer.js +122 -0
  11. package/dist/CmrComponents/niivue-viewer/LayersPanel.d.ts +1 -0
  12. package/dist/CmrComponents/niivue-viewer/LayersPanel.js +107 -0
  13. package/dist/CmrComponents/niivue-viewer/Niivue.css +8 -0
  14. package/dist/CmrComponents/niivue-viewer/NiivuePatcher.d.ts +2 -0
  15. package/dist/CmrComponents/niivue-viewer/NiivuePatcher.js +1402 -0
  16. package/dist/CmrComponents/niivue-viewer/NumberPicker.d.ts +1 -0
  17. package/dist/CmrComponents/niivue-viewer/NumberPicker.js +40 -0
  18. package/dist/CmrComponents/niivue-viewer/SettingsPanel.d.ts +1 -0
  19. package/dist/CmrComponents/niivue-viewer/SettingsPanel.js +30 -0
  20. package/dist/CmrComponents/niivue-viewer/Switch.d.ts +1 -0
  21. package/dist/CmrComponents/niivue-viewer/Switch.js +27 -0
  22. package/dist/CmrComponents/niivue-viewer/Toolbar.d.ts +48 -0
  23. package/dist/CmrComponents/niivue-viewer/Toolbar.js +276 -0
  24. package/dist/CmrComponents/niivue-viewer/Toolbar.scss +40 -0
  25. package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/DrawColorPlatte.d.ts +2 -0
  26. package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/DrawColorPlatte.js +61 -0
  27. package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/EraserPlatte.d.ts +2 -0
  28. package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/EraserPlatte.js +56 -0
  29. package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/MaskPlatte.d.ts +2 -0
  30. package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/MaskPlatte.js +148 -0
  31. package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/MroDrawToolkit.d.ts +1 -0
  32. package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/MroDrawToolkit.js +177 -0
  33. package/dist/CmrComponents/niivue-viewer/niivuePenType.d.ts +10 -0
  34. package/dist/CmrComponents/niivue-viewer/niivuePenType.js +10 -0
  35. package/dist/core/common/components/NiivueTools/NiivuePatcher.d.ts +2 -0
  36. package/dist/core/common/components/NiivueTools/components/ControlThemes.d.ts +1 -0
  37. package/dist/core/common/components/NiivueTools/components/ControlThemes.js +123 -0
  38. package/dist/core/common/components/NiivueTools/components/Example.d.ts +10 -0
  39. package/dist/core/common/components/NiivueTools/components/Example.js +326 -0
  40. package/dist/core/common/components/NiivueTools/components/ImageList.d.ts +1 -0
  41. package/dist/core/common/components/NiivueTools/components/ImageList.js +22 -0
  42. package/dist/core/common/components/NiivueTools/components/ImageListItem.d.ts +1 -0
  43. package/dist/core/common/components/NiivueTools/components/ImageListItem.js +103 -0
  44. package/dist/core/common/components/NiivueTools/main.d.ts +1 -0
  45. package/dist/core/common/components/NiivueTools/main.js +16 -0
  46. package/dist/core/common/components/NiivueTools/util.d.ts +21 -0
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.js +2 -0
  49. package/package.json +3 -3
@@ -0,0 +1,1402 @@
1
+ /**
2
+ * This file patches the original NiiVue library to produce customized behaviors and effects.
3
+ */
4
+ import { Niivue, NVImage, NVImageFromUrlOptions } from "@niivue/niivue";
5
+
6
+ /*
7
+ * bitmapOverlay — in-browser state only: remembers each voxel's label *before* Group
8
+ * (so Ungroup can restore red/green etc.). It is not written to NIfTI; closing the tab
9
+ * or loading a new drawing loses it unless the file already stores separate label ids.
10
+ *
11
+ * When to drop that saved state (so it cannot go stale):
12
+ * - Full reset: replace whole drawing, clear/close drawing, delete-by-label,
13
+ * load from URL/base64, relabel → clear the entire array.
14
+ * - Merge upload: remove entries only for voxels the import overwrites (non-zero in
15
+ * the imported map); other voxels keep their saved pre-merge labels for Ungroup.
16
+ */
17
+ const bitmapOverlay = [];
18
+
19
+ function clearBitmapOverlay() {
20
+ bitmapOverlay.length = 0;
21
+ }
22
+
23
+ /** Remove group-undo rows for voxels that will be / were overwritten by an import (flat indices). */
24
+ function stripBitmapOverlayForVoxelIndices(writtenSet) {
25
+ if (!writtenSet || writtenSet.size === 0) {
26
+ return;
27
+ }
28
+ for (let k = bitmapOverlay.length - 1; k >= 0; k--) {
29
+ if (writtenSet.has(bitmapOverlay[k][0])) {
30
+ bitmapOverlay.splice(k, 1);
31
+ }
32
+ }
33
+ }
34
+
35
+ /** Clone of drawBitmap with bitmapOverlay undo applied; does not mutate the live drawing. */
36
+ function getResolvedDrawBitmapForExport(nv) {
37
+ const src = nv.drawBitmap;
38
+ const copy = new Uint8Array(src);
39
+ for (let i = 0; i < bitmapOverlay.length; i++) {
40
+ const t = bitmapOverlay[i];
41
+ copy[t[0]] = t[1];
42
+ }
43
+ return copy;
44
+ }
45
+
46
+ var NiivueObject3D = function (id, vertexBuffer, mode, indexCount, indexBuffer = null, vao = null) {
47
+ this.BLEND = 1;
48
+ this.CULL_FACE = 2;
49
+ this.CULL_FRONT = 4;
50
+ this.CULL_BACK = 8;
51
+ this.ENABLE_DEPTH_TEST = 16;
52
+ this.sphereIdx = [];
53
+ this.sphereVtx = [];
54
+ this.renderShaders = [];
55
+ this.isVisible = true;
56
+ this.isPickable = true;
57
+ this.vertexBuffer = vertexBuffer;
58
+ this.indexCount = indexCount;
59
+ this.indexBuffer = indexBuffer;
60
+ this.vao = vao;
61
+ this.mode = mode;
62
+ this.glFlags = 0;
63
+ this.id = id;
64
+ this.colorId = [
65
+ ((id >> 0) & 255) / 255,
66
+ ((id >> 8) & 255) / 255,
67
+ ((id >> 16) & 255) / 255,
68
+ ((id >> 24) & 255) / 255
69
+ ];
70
+ this.modelMatrix = create2();
71
+ this.scale = [1, 1, 1];
72
+ this.position = [0, 0, 0];
73
+ this.rotation = [0, 0, 0];
74
+ this.rotationRadians = 0;
75
+ this.extentsMin = [];
76
+ this.extentsMax = [];
77
+ };
78
+
79
+
80
+ function create2() {
81
+ var out = new Float32Array(16);
82
+ out[0] = 1;
83
+ out[5] = 1;
84
+ out[10] = 1;
85
+ out[15] = 1;
86
+ return out;
87
+ }
88
+
89
+ function decodeRLE(rle, decodedlen) {
90
+ const r = new Uint8Array(rle.buffer)
91
+ const rI = new Int8Array(r.buffer) // typecast as header can be negative
92
+ let rp = 0 // input position in rle array
93
+ // d: output uncompressed data array
94
+ const d = new Uint8Array(decodedlen)
95
+ let dp = 0 // output position in decoded array
96
+ while (rp < r.length) {
97
+ // read header
98
+ const hdr = rI[rp]
99
+ rp++
100
+ if (hdr < 0) {
101
+ // write run
102
+ const v = rI[rp]
103
+ rp++
104
+ for (let i = 0; i < 1 - hdr; i++) {
105
+ d[dp] = v
106
+ dp++
107
+ }
108
+ } else {
109
+ // write literal
110
+ for (let i = 0; i < hdr + 1; i++) {
111
+ d[dp] = rI[rp]
112
+ rp++
113
+ dp++
114
+ }
115
+ }
116
+ }
117
+ return d
118
+ }
119
+
120
+ function intensityRaw2Scaled(hdr, raw) {
121
+ if (hdr.scl_slope === 0) {
122
+ hdr.scl_slope = 1.0
123
+ }
124
+ return raw * hdr.scl_slope + hdr.scl_inter
125
+ }
126
+
127
+ const SLICE_TYPE = Object.freeze({
128
+ AXIAL: 0,
129
+ CORONAL: 1,
130
+ SAGITTAL: 2,
131
+ MULTIPLANAR: 3,
132
+ RENDER: 4,
133
+ });
134
+
135
+ const labelVisibility = {};
136
+ Niivue.prototype.getLabelVisibility = function (label) {
137
+ if (labelVisibility[label] === undefined) {
138
+ labelVisibility[label] = true;
139
+ return true;
140
+ }
141
+ else {
142
+ return labelVisibility[label];
143
+ }
144
+ }
145
+
146
+ Niivue.prototype.setLabelVisibility = function (label, visible) {
147
+ console.log(label);
148
+ labelVisibility[label] = visible;
149
+ if (this.hiddenBitmap === undefined || this.hiddenBitmap === null || this.hiddenBitmap.length === 0)
150
+ this.hiddenBitmap = new Uint8Array(this.drawBitmap.length);
151
+ if (!visible) {
152
+ for (let i = 0; i < this.drawBitmap.length; i++) {
153
+ if (this.drawBitmap[i] === label) {
154
+ console.log(this.drawBitmap[i])
155
+ this.hiddenBitmap[i] = label;
156
+ this.drawBitmap[i] = 0;
157
+ }
158
+ }
159
+ this.refreshDrawing(false);
160
+ } else {
161
+ for (let i = 0; i < this.hiddenBitmap.length; i++) {
162
+ if (this.hiddenBitmap[i] === label) {
163
+ this.hiddenBitmap[i] = 0;
164
+ if (this.drawBitmap[i] === 0) {
165
+ this.drawBitmap[i] = label;
166
+ }
167
+ }
168
+ }
169
+ this.refreshDrawing(false);
170
+ }
171
+ }
172
+
173
+ // whether the user drags, resets, or changes contrast programmatically, React will be notified
174
+ const _refreshLayers = Niivue.prototype.refreshLayers;
175
+ Niivue.prototype.refreshLayers = function (...args) {
176
+ const r = _refreshLayers.apply(this, args);
177
+ if (typeof this.onIntensityChange === "function") {
178
+ this.onIntensityChange(this.volumes?.[0]);
179
+ }
180
+ return r;
181
+ };
182
+
183
+
184
+ // const drawAddUndoBitmap = Niivue.prototype.drawAddUndoBitmap;
185
+ // // This patching adds visibility filtering to drawings
186
+ // Niivue.prototype.drawAddUndoBitmap = async function(){
187
+ // let resp = await drawAddUndoBitmap.call(this);
188
+ // return resp;
189
+ // }
190
+
191
+ // This patch to closeDrawing clears invisible bitmapCache when applied
192
+ const closeDrawing = Niivue.prototype.closeDrawing;
193
+ /**
194
+ * This patch to closeDrawing clears invisible bitmapCache when applied
195
+ */
196
+ Niivue.prototype.closeDrawing = function () {
197
+ clearBitmapOverlay();
198
+ if (this.drawBitmap !== undefined && this.drawBitmap !== null)
199
+ this.hiddenBitmap = new Uint8Array(this.drawBitmap.length);
200
+ else if (this.hiddenBitmap !== undefined)
201
+ this.hiddenBitmap = [];
202
+ closeDrawing.call(this);
203
+ }
204
+
205
+ const _loadDrawingFromUrl = Niivue.prototype.loadDrawingFromUrl;
206
+ Niivue.prototype.loadDrawingFromUrl = async function (fnm, isBinarize) {
207
+ clearBitmapOverlay();
208
+ return _loadDrawingFromUrl.call(this, fnm, isBinarize);
209
+ };
210
+
211
+ /**
212
+ * The difference between clear drawing and close drawing is that
213
+ * clear drawing retains itself in the undo stack.
214
+ */
215
+ Niivue.prototype.clearDrawing = function () {
216
+ clearBitmapOverlay();
217
+ if (this.drawBitmap != undefined && this.drawBitmap != null) {
218
+ this.drawBitmap = new Uint8Array(this.drawBitmap.length);
219
+ this.hiddenBitmap = new Uint8Array(this.drawBitmap.length);
220
+ }
221
+ this.drawAddUndoBitmap();
222
+ this.refreshDrawing(true)
223
+ }
224
+
225
+ // not included in public docs
226
+ // set color of single voxel in drawing
227
+ // Include thickness in opts
228
+ /**
229
+ * Pen bounds specify the padding added
230
+ * around the drawing center
231
+ * @param x
232
+ * @param y
233
+ * @param z
234
+ * @param penValue
235
+ */
236
+ Niivue.prototype.drawPt = function (x, y, z, penValue) {
237
+ const penBounds = this.opts.penBounds ? this.opts.penBounds : 0;
238
+ const dx = this.back.dims[1]
239
+ const dy = this.back.dims[2]
240
+ const dz = this.back.dims[3]
241
+ //Sweep through cubic area, filter by radius
242
+ for (let i = x - penBounds; i <= x + penBounds; i++) {
243
+ for (let j = y - penBounds; j <= y + penBounds; j++) {
244
+ for (let k = z - penBounds; k <= z + penBounds; k++) {
245
+ // (penBounds+1)*(penBounds) as radius filter makes better circles in discrete case
246
+ if ((i - x) * (i - x) + (j - y) * (j - y) + (k - z) * (k - z) <= (penBounds + 1) * (penBounds)) {
247
+ let xn = Math.min(Math.max(i, 0), dx - 1)
248
+ let yn = Math.min(Math.max(j, 0), dy - 1)
249
+ let zn = Math.min(Math.max(k, 0), dz - 1)
250
+ this.drawBitmap[xn + yn * dx + zn * dx * dy] = penValue;
251
+ }
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ // not included in public docs
258
+ // given series of line segments, connect first and last
259
+ // voxel and fill the interior of the line segments
260
+ // yuelong: fill volumetric interior of the paint space,
261
+ // if active draw pen set to invisible
262
+ Niivue.prototype.drawPenFilled = function () {
263
+ const nPts = this.drawPenFillPts.length
264
+ if (nPts < 2) {
265
+ // can not fill single line
266
+ this.drawPenFillPts = []
267
+ return
268
+ }
269
+ // do fill in 2D, based on axial (0), coronal (1) or sagittal drawing (2
270
+ const axCorSag = this.drawPenAxCorSag
271
+ // axial is x(0)*y(1) horizontal*vertical
272
+ let h = 0
273
+ let v = 1
274
+ if (axCorSag === 1) {
275
+ v = 2
276
+ } // coronal is x(0)*z(0)
277
+ if (axCorSag === 2) {
278
+ // sagittal is y(1)*z(2)
279
+ h = 1
280
+ v = 2
281
+ }
282
+ const penBounds = this.opts.penBounds ? this.opts.penBounds : 0;
283
+ const w = penBounds * 2 + 1; // Yuelong: paint fill with thickness
284
+ const dims3D = [this.back.dims[h + 1], this.back.dims[v + 1], w] // +1: dims indexed from 0!
285
+ // create bitmap of horizontal*vertical voxels:
286
+ const img3D = new Uint8Array(dims3D[0] * dims3D[1] * dims3D[2])
287
+ let pen = 1 // do not use this.opts.penValue, as "erase" is zero
288
+ function drawPt3D(x, y, penValue) {
289
+ const dx = dims3D[0]
290
+ const dy = dims3D[1]
291
+ const dz = w
292
+ const z = (w - 1) / 2;
293
+ //Sweep through cubic area, filter by radius
294
+ for (let i = x - penBounds; i <= x + penBounds; i++) {
295
+ for (let j = y - penBounds; j <= y + penBounds; j++) {
296
+ for (let k = z - penBounds; k <= z + penBounds; k++) {
297
+ // (penBounds+1)*(penBounds) as radius filter makes better circles in discrete case
298
+ if ((i - x) * (i - x) + (j - y) * (j - y) + (k - z) * (k - z) <= (penBounds + 1) * (penBounds)) {
299
+ let xn = Math.min(Math.max(i, 0), dx - 1)
300
+ let yn = Math.min(Math.max(j, 0), dy - 1)
301
+ let zn = Math.min(Math.max(k, 0), dz - 1)
302
+ img3D[xn + yn * dx + zn * dx * dy] = penValue;
303
+ }
304
+ }
305
+ }
306
+ }
307
+ }
308
+ function drawLine2D(ptA, ptB /* penValue */) {
309
+ const dx = Math.abs(ptA[0] - ptB[0])
310
+ const dy = Math.abs(ptA[1] - ptB[1])
311
+ // img2D[ptA[0] + ptA[1] * dims2D[0]] = pen
312
+ // img2D[ptB[0] + ptB[1] * dims2D[0]] = pen
313
+ drawPt3D(ptA[0], ptA[1], pen);
314
+ drawPt3D(ptB[0], ptB[1], pen);
315
+ let xs = -1
316
+ let ys = -1
317
+ if (ptB[0] > ptA[0]) {
318
+ xs = 1
319
+ }
320
+ if (ptB[1] > ptA[1]) {
321
+ ys = 1
322
+ }
323
+ let x1 = ptA[0]
324
+ let y1 = ptA[1]
325
+ const x2 = ptB[0]
326
+ const y2 = ptB[1]
327
+ if (dx >= dy) {
328
+ // Driving axis is X-axis"
329
+ let p1 = 2 * dy - dx
330
+ while (x1 !== x2) {
331
+ x1 += xs
332
+ if (p1 >= 0) {
333
+ y1 += ys
334
+ p1 -= 2 * dx
335
+ }
336
+ p1 += 2 * dy
337
+ drawPt3D(x1, y1, pen);
338
+ }
339
+ } else {
340
+ // Driving axis is Y-axis"
341
+ let p1 = 2 * dx - dy
342
+ while (y1 !== y2) {
343
+ y1 += ys
344
+ if (p1 >= 0) {
345
+ x1 += xs
346
+ p1 -= 2 * dy
347
+ }
348
+ p1 += 2 * dx
349
+ drawPt3D(x1, y1, pen);
350
+ }
351
+ }
352
+ }
353
+ const startPt = [this.drawPenFillPts[0][h], this.drawPenFillPts[0][v]]
354
+ let prevPt = startPt
355
+ for (let i = 1; i < nPts; i++) {
356
+ const pt = [this.drawPenFillPts[i][h], this.drawPenFillPts[i][v]]
357
+ drawLine2D(prevPt, pt)
358
+ prevPt = pt
359
+ }
360
+ drawLine2D(startPt, prevPt) // close drawing
361
+ // flood fill
362
+ const seeds = []
363
+ function setSeed(pt) { // pt 2D -> 3D
364
+ if (pt[0] < 0 || pt[1] < 0 || pt[2] < 0 || pt[0] >= dims3D[0] || pt[1] >= dims3D[1] || pt[3] >= dims3D[2]) {
365
+ return
366
+ }
367
+ const pxl = pt[0] + pt[1] * dims3D[0] + pt[2] * dims3D[0] * dims3D[1]
368
+ if (img3D[pxl] !== 0) {
369
+ return
370
+ } // not blank
371
+ seeds.push(pt)
372
+ img3D[pxl] = 2
373
+ }
374
+ // https://en.wikipedia.org/wiki/Flood_fill
375
+ // first seed all edges
376
+ // // bottom row
377
+ // for (let i = 0; i < dims2D[0]; i++) {
378
+ // setSeed([i, 0])
379
+ // }
380
+ // // top row
381
+ // for (let i = 0; i < dims2D[0]; i++) {
382
+ // setSeed([i, dims2D[1] - 1])
383
+ // }
384
+ // // left column
385
+ // for (let i = 0; i < dims2D[1]; i++) {
386
+ // setSeed([0, i])
387
+ // }
388
+ // // right columns
389
+ // for (let i = 0; i < dims2D[1]; i++) {
390
+ // setSeed([dims2D[0] - 1, i])
391
+ // }
392
+ // Yuelong: Instead of seeding all edges, seed eight corners of the bounding box,
393
+ setSeed([0, 0, 0]);
394
+ setSeed([dims3D[0] - 1, 0, 0]);
395
+ setSeed([0, dims3D[1] - 1, 0]);
396
+ setSeed([dims3D[0] - 1, dims3D[1] - 1, 0]);
397
+ setSeed([0, 0, dims3D[2] - 1]);
398
+ setSeed([dims3D[0] - 1, 0, dims3D[2] - 1]);
399
+ setSeed([0, dims3D[1] - 1, dims3D[2] - 1]);
400
+ setSeed([dims3D[0] - 1, dims3D[1] - 1, dims3D[2] - 1]);
401
+ // now retire first in first out
402
+ while (seeds.length > 0) {
403
+ // always remove one seed, plant 0..4 new ones
404
+ const seed = seeds.shift()
405
+ setSeed([seed[0] - 1, seed[1], seed[2]])
406
+ setSeed([seed[0] + 1, seed[1], seed[2]])
407
+ setSeed([seed[0], seed[1] - 1, seed[2]])
408
+ setSeed([seed[0], seed[1] + 1, seed[2]])
409
+ // Yuelong: flood fill z-axis as well
410
+ setSeed([seed[0], seed[1], seed[2] + 1])
411
+ setSeed([seed[0], seed[1], seed[2] - 1])
412
+ }
413
+ // all voxels with value of zero have no path to edges
414
+ // insert surviving pixels from 2D bitmap into 3D bitmap
415
+ pen = this.opts.penValue
416
+ const slice = this.drawPenFillPts[0][3 - (h + v)]
417
+ // if (axCorSag === 0) {
418
+ // // axial
419
+ // const offset = slice * dims2D[0] * dims2D[1]
420
+ // for (let i = 0; i < dims2D[0] * dims2D[1]; i++) {
421
+ // if (img2D[i] !== 2) {
422
+ // this.drawBitmap[i + offset] = pen
423
+ // }
424
+ // }
425
+ // } else {
426
+ // let xStride = 1 // coronal: horizontal LR pixels contiguous
427
+ // const yStride = this.back.dims[1] * this.back.dims[2] // coronal: vertical is slice
428
+ // let zOffset = slice * this.back.dims[1] // coronal: slice is number of columns
429
+ // if (axCorSag === 2) {
430
+ // // sagittal
431
+ // xStride = this.back.dims[1]
432
+ // zOffset = slice
433
+ // }
434
+ // let i = 0
435
+ // for (let y = 0; y < dims2D[1]; y++) {
436
+ // for (let x = 0; x < dims2D[0]; x++) {
437
+ // if (img2D[i] !== 2) {
438
+ // this.drawBitmap[x * xStride + y * yStride + zOffset] = pen
439
+ // }
440
+ // i++
441
+ // }
442
+ // }
443
+ // }
444
+ // Stride with permutation symmetry
445
+ let strides = [1, this.back.dims[1], this.back.dims[1] * this.back.dims[2]];
446
+ // xStride = s0 for axial and coronal, s1 for sagital
447
+ let xStride = (axCorSag == 2) ? strides[1] : strides[0];
448
+ // yStride = s1 for axial, s2 for coronal and sagital
449
+ const yStride = (axCorSag == 0) ? strides[1] : strides[2];
450
+ // zStride = s2 for axial, s1 for coronal, s0 for sagital
451
+ const zStride = strides[2 - axCorSag];
452
+ const zOffset = slice * zStride;
453
+ let i = 0
454
+ for (let z = -penBounds; z <= penBounds; z++) {
455
+ for (let y = 0; y < dims3D[1]; y++) {
456
+ for (let x = 0; x < dims3D[0]; x++) {
457
+ if (img3D[i] !== 2) {
458
+ // Fill by 3D traversal
459
+ this.drawBitmap[x * xStride + y * yStride + zOffset + z * zStride] = pen
460
+ }
461
+ i++
462
+ }
463
+ }
464
+ }
465
+ // this.drawUndoBitmaps[this.currentDrawUndoBitmap]
466
+ if (!this.drawFillOverwrites && this.drawUndoBitmaps[this.currentDrawUndoBitmap].length > 0) {
467
+ const nv = this.drawBitmap.length
468
+ const bmp = decodeRLE(this.drawUndoBitmaps[this.currentDrawUndoBitmap], nv)
469
+ for (let i = 0; i < nv; i++) {
470
+ if (bmp[i] === 0) {
471
+ continue
472
+ }
473
+ this.drawBitmap[i] = bmp[i]
474
+ }
475
+ }
476
+ this.drawPenFillPts = []
477
+ // First imprint all hiddenBitmaps into the draw bitmap,
478
+ // visible voxels take precedence
479
+ if (this.hiddenBitmap)
480
+ this.hiddenBitmap.map((value, index) => {
481
+ if (value !== 0 && this.drawBitmap[index] === 0) {
482
+ this.drawBitmap[index] = value;
483
+ }
484
+ })
485
+ this.drawAddUndoBitmap()
486
+ // Post-processing to hide hidden voxels
487
+ this.hiddenBitmap = new Uint8Array(this.drawBitmap.length);
488
+ for (let i = 0; i < this.drawBitmap.length; i++) {
489
+ let pen = this.drawBitmap[i];
490
+ if (!this.getLabelVisibility(pen)) {
491
+ this.hiddenBitmap[i] = this.drawBitmap[i];
492
+ this.drawBitmap[i] = 0;
493
+ }
494
+ }
495
+ this.refreshDrawing(false)
496
+ }
497
+
498
+ Niivue.prototype.fillRange = function (min, max, penValue, inverted = false,
499
+ original = undefined, setOriginal = (original) => { }) {
500
+ console.log(this.volumes);
501
+ let volume = this.volumes[0];
502
+ if (volume == undefined) {
503
+ return;
504
+ }
505
+ if (!this.drawBitmap) {
506
+ this.createEmptyDrawing();
507
+ }
508
+ // First load underlying imprinting
509
+ if (original == undefined) {
510
+ setOriginal([...this.drawBitmap]);
511
+ } else {
512
+ this.drawBitmap = new Uint8Array(original);
513
+ }
514
+ console.log(volume.img.length);
515
+ console.log(this.drawBitmap.length);
516
+ // Next write into the drawbitmap where voxel value are within range,
517
+ // no need to write into hidden bitmap specifically due to the post-processing
518
+ // step, where draw bitmap values will be hidden accordingly
519
+
520
+ const dx = this.back.dims[1]
521
+ const dy = this.back.dims[2]
522
+ const dz = this.back.dims[3]
523
+ for (let x = 0; x < dx; x++)
524
+ for (let y = 0; y < dy; y++)
525
+ for (let z = 0; z < dz; z++) {
526
+ let i = x + y * dx + z * dx * dy;
527
+ let val = volume.getValue(x, y, z);
528
+ if ((!inverted && (min <= val && max >= val)) ||
529
+ //Note here that e-4 is not a trivial value,
530
+ // we can do this here because ranges beneath e-4 will be rescaled inside
531
+ // the checkRange function inside Niivue.jsx. It is still not entirely safe
532
+ // but a necessary step for inclusive range checking under float inprecisions
533
+ (inverted && (min >= val || max < val))) {
534
+ // console.log('filling');
535
+ this.drawBitmap[i] = penValue;
536
+ }
537
+ }
538
+ // Next update drawUndoBitmaps pipeline
539
+ // First imprint all hiddenBitmaps into the draw bitmap,
540
+ // visible voxels take precedence
541
+ if (this.hiddenBitmap)
542
+ this.hiddenBitmap.map((value, index) => {
543
+ if (value !== 0 && this.drawBitmap[index] === 0) {
544
+ this.drawBitmap[index] = value;
545
+ }
546
+ })
547
+ // Post-processing to hide invisible voxels
548
+ this.hiddenBitmap = new Uint8Array(this.drawBitmap.length);
549
+ for (let i = 0; i < this.drawBitmap.length; i++) {
550
+ let pen = this.drawBitmap[i];
551
+ if (!this.getLabelVisibility(pen)) {
552
+ this.hiddenBitmap[i] = this.drawBitmap[i];
553
+ this.drawBitmap[i] = 0;
554
+ }
555
+ }
556
+ this.refreshDrawing(true)
557
+ // props.nv.drawScene()
558
+ }
559
+
560
+ Niivue.prototype.drawAddUndoBitmapWithHiddenVoxels = function () {
561
+ // Next update drawUndoBitmaps pipeline
562
+ // First imprint all hiddenBitmaps into the draw bitmap,
563
+ // visible voxels take precedence
564
+ if (this.hiddenBitmap)
565
+ this.hiddenBitmap.map((value, index) => {
566
+ if (value !== 0 && this.drawBitmap[index] === 0) {
567
+ this.drawBitmap[index] = value;
568
+ }
569
+ })
570
+ this.drawAddUndoBitmap()
571
+ // Post-processing to hide invisible voxels
572
+ this.hiddenBitmap = new Uint8Array(this.drawBitmap.length);
573
+ for (let i = 0; i < this.drawBitmap.length; i++) {
574
+ let pen = this.drawBitmap[i];
575
+ if (!this.getLabelVisibility(pen)) {
576
+ this.hiddenBitmap[i] = this.drawBitmap[i];
577
+ this.drawBitmap[i] = 0;
578
+ }
579
+ }
580
+ }
581
+
582
+ /**
583
+ * Restore drawing to previous state
584
+ * @example niivue.drawUndo();
585
+ * @see {@link https://niivue.github.io/niivue/features/draw.ui.html|live demo usage}
586
+ * Yuelong: drawundo hides invisible rois in post processing
587
+ */
588
+ Niivue.prototype.drawUndo = function () {
589
+ if (this.drawUndoBitmaps.length < 1) {
590
+ console.debug('undo bitmaps not loaded')
591
+ return
592
+ }
593
+ this.currentDrawUndoBitmap--
594
+ if (this.currentDrawUndoBitmap < 0) {
595
+ this.currentDrawUndoBitmap = this.drawUndoBitmaps.length - 1
596
+ }
597
+ if (this.currentDrawUndoBitmap >= this.drawUndoBitmaps.length) {
598
+ this.currentDrawUndoBitmap = 0
599
+ }
600
+ if (this.drawUndoBitmaps[this.currentDrawUndoBitmap].length < 2) {
601
+ console.debug('drawUndo is misbehaving')
602
+ return
603
+ }
604
+ this.drawBitmap = decodeRLE(this.drawUndoBitmaps[this.currentDrawUndoBitmap], this.drawBitmap.length)
605
+ // Post-processing to hide invisible region and reveal hidden ones
606
+ this.hiddenBitmap = new Uint8Array(this.drawBitmap.length);
607
+ for (let i = 0; i < this.drawBitmap.length; i++) {
608
+ let pen = this.drawBitmap[i];
609
+ if (!this.getLabelVisibility(pen)) {
610
+ this.hiddenBitmap[i] = this.drawBitmap[i];
611
+ this.drawBitmap[i] = 0;
612
+ }
613
+ }
614
+ this.refreshDrawing(true)
615
+ }
616
+
617
+ /**
618
+ * save voxel-based image to disk
619
+ * @param {string} fnm filename of NIfTI image to create
620
+ * @param {boolean} [false] isSaveDrawing determines whether drawing or background image is saved
621
+ * @param {number} [0] volumeByIndex determines layer to save (0 for background)
622
+ * @param {number} [0] volumeByIndex determines layer to save (0 for background)
623
+ * @example niivue.saveImage('test.nii', true);
624
+ * @see {@link https://niivue.github.io/niivue/features/draw.ui.html|live demo usage}
625
+ */
626
+ Niivue.prototype.saveImageByLabels = async function (fnm, labels = [1]) {
627
+ if (this.back.dims === undefined) {
628
+ console.debug('No voxelwise image open')
629
+ return false
630
+ }
631
+ if (!this.drawBitmap) {
632
+ console.debug('No drawing open')
633
+ return false
634
+ }
635
+ const live = this.drawBitmap
636
+ const resolved = getResolvedDrawBitmapForExport(this)
637
+ const perm = this.volumes[0].permRAS
638
+ if (perm[0] === 1 && perm[1] === 2 && perm[2] === 3) {
639
+ const outFlat = new Uint8Array(live.length)
640
+ for (let i = 0; i < live.length; i++) {
641
+ outFlat[i] = labels.indexOf(live[i]) >= 0 ? resolved[i] : 0
642
+ }
643
+ await this.volumes[0].saveToDisk(fnm, outFlat)
644
+ return true
645
+ } else {
646
+ const dims = this.volumes[0].hdr.dims // reverse to original
647
+ // reverse RAS to native space, layout is mrtrix MIF format
648
+ // for details see NVImage.readMIF()
649
+ const layout = [0, 0, 0]
650
+ for (let i = 0; i < 3; i++) {
651
+ for (let j = 0; j < 3; j++) {
652
+ if (Math.abs(perm[i]) - 1 !== j) continue
653
+ layout[j] = i * Math.sign(perm[i])
654
+ }
655
+ }
656
+ let stride = 1
657
+ const instride = [1, 1, 1]
658
+ const inflip = [false, false, false]
659
+ for (let i = 0; i < layout.length; i++) {
660
+ for (let j = 0; j < layout.length; j++) {
661
+ const a = Math.abs(layout[j])
662
+ if (a !== i) continue
663
+ instride[j] = stride
664
+ // detect -0: https://medium.com/coding-at-dawn/is-negative-zero-0-a-number-in-javascript-c62739f80114
665
+ if (layout[j] < 0 || Object.is(layout[j], -0)) inflip[j] = true
666
+ stride *= dims[j + 1]
667
+ }
668
+ }
669
+ // lookup table for flips and stride offsets:
670
+ const range = (start, stop, step) =>
671
+ Array.from({ length: (stop - start) / step + 1 }, (_, i) => start + i * step)
672
+ let xlut = range(0, dims[1] - 1, 1)
673
+ if (inflip[0]) xlut = range(dims[1] - 1, 0, -1)
674
+ for (let i = 0; i < dims[1]; i++) xlut[i] *= instride[0]
675
+ let ylut = range(0, dims[2] - 1, 1)
676
+ if (inflip[1]) ylut = range(dims[2] - 1, 0, -1)
677
+ for (let i = 0; i < dims[2]; i++) ylut[i] *= instride[1]
678
+ let zlut = range(0, dims[3] - 1, 1)
679
+ if (inflip[2]) zlut = range(dims[3] - 1, 0, -1)
680
+ for (let i = 0; i < dims[3]; i++) zlut[i] *= instride[2]
681
+ // convert data — mask by current (possibly merged) label; write underlying ungrouped IDs
682
+
683
+ const outVs = new Uint8Array(dims[1] * dims[2] * dims[3])
684
+ let j = 0
685
+ for (let z = 0; z < dims[3]; z++) {
686
+ for (let y = 0; y < dims[2]; y++) {
687
+ for (let x = 0; x < dims[1]; x++) {
688
+ const idx = xlut[x] + ylut[y] + zlut[z]
689
+ const bitLive = live[idx]
690
+ outVs[j] = (labels.indexOf(bitLive) >= 0) ? resolved[idx] : 0
691
+ j++
692
+ }
693
+ }
694
+ }
695
+ await this.volumes[0].saveToDisk(fnm, outVs)
696
+ return true
697
+ }
698
+ }
699
+
700
+ Niivue.prototype.deleteDrawingByLabel = function (labels = [0]) {
701
+ clearBitmapOverlay();
702
+ for (let i = 0; i < this.drawBitmap.length; i++) {
703
+ this.drawBitmap[i] = (labels.indexOf(this.drawBitmap[i]) < 0) ? this.drawBitmap[i] : 0;
704
+ }
705
+ this.refreshDrawing(false);
706
+ }
707
+
708
+ Niivue.prototype.groupLabelsInto = function (sourceLabels = [0], targetLabel = 7) {
709
+ for (let i = 0; i < this.drawBitmap.length; i++) {
710
+ if (sourceLabels.indexOf(this.drawBitmap[i]) >= 0) {
711
+ for (let k = bitmapOverlay.length - 1; k >= 0; k--) {
712
+ if (bitmapOverlay[k][0] === i) {
713
+ bitmapOverlay.splice(k, 1);
714
+ }
715
+ }
716
+ bitmapOverlay.push([i, this.drawBitmap[i]]);
717
+ this.drawBitmap[i] = targetLabel;
718
+ }
719
+ }
720
+ this.refreshDrawing(false);
721
+ }
722
+
723
+ Niivue.prototype.ungroup = function () {
724
+ for (let tuple of bitmapOverlay) {
725
+ this.drawBitmap[tuple[0]] = tuple[1];
726
+ }
727
+ bitmapOverlay.length = 0;
728
+ this.refreshDrawing(false);
729
+ }
730
+
731
+ /**
732
+ * Smart group from ROI table selection.
733
+ * - Extend: selection includes merged label(s) >= targetLabel → mask those voxels, ungroup once, merge into max(merged ids in selection) (e.g. add to 7 or 8).
734
+ * - New merge (primitives only): if no merged id >= targetLabel exists in the volume, merge into targetLabel (7); else merge into maxMergedInVolume + 1 (e.g. second group → 8).
735
+ */
736
+ Niivue.prototype.groupLabelsFromSelection = function (sourceLabels = [], targetLabel = 7) {
737
+ if (!this.drawBitmap || !sourceLabels || sourceLabels.length < 2) {
738
+ return;
739
+ }
740
+ const labelSet = new Set(sourceLabels);
741
+ if (labelSet.size < 2) {
742
+ return;
743
+ }
744
+ const MERGE_THRESHOLD = targetLabel;
745
+ const mergedInSelection = sourceLabels.filter(function (l) {
746
+ return l >= MERGE_THRESHOLD;
747
+ });
748
+ if (mergedInSelection.length > 0) {
749
+ const mergeTarget = Math.max.apply(null, mergedInSelection);
750
+ const n = this.drawBitmap.length;
751
+ const mask = new Uint8Array(n);
752
+ for (let i = 0; i < n; i++) {
753
+ if (labelSet.has(this.drawBitmap[i])) {
754
+ mask[i] = 1;
755
+ }
756
+ }
757
+ this.ungroup();
758
+ const mergeSet = new Set();
759
+ for (let i = 0; i < n; i++) {
760
+ if (mask[i] && this.drawBitmap[i] !== 0) {
761
+ mergeSet.add(this.drawBitmap[i]);
762
+ }
763
+ }
764
+ const toMerge = Array.from(mergeSet).sort(function (a, b) { return a - b; });
765
+ if (toMerge.length === 0) {
766
+ this.refreshDrawing(false);
767
+ return;
768
+ }
769
+ this.groupLabelsInto(toMerge, mergeTarget);
770
+ return;
771
+ }
772
+ var sceneMaxMerged = 0;
773
+ for (var j = 0; j < this.drawBitmap.length; j++) {
774
+ var v = this.drawBitmap[j];
775
+ if (v >= MERGE_THRESHOLD) {
776
+ sceneMaxMerged = Math.max(sceneMaxMerged, v);
777
+ }
778
+ }
779
+ var newTarget = sceneMaxMerged >= MERGE_THRESHOLD ? sceneMaxMerged + 1 : MERGE_THRESHOLD;
780
+ this.groupLabelsInto(sourceLabels, newTarget);
781
+ };
782
+
783
+ Niivue.prototype.resetScene = function () {
784
+ this.scene.pan2Dxyzmm = [0, 0, 0, 1]
785
+ this.drawScene();
786
+ }
787
+
788
+ Niivue.prototype.recenter = function () {
789
+ // this.scene.pan2Dxyzmm[0] = 0;
790
+ // this.scene.pan2Dxyzmm[1] = 0;
791
+ // this.scene.pan2Dxyzmm[2] = 0;
792
+
793
+ const zoom = this.scene.pan2Dxyzmm[3];
794
+ this.scene.pan2Dxyzmm = [0, 0, 0, 1];
795
+ const zoomChange = this.scene.pan2Dxyzmm[3] - zoom
796
+ this.scene.pan2Dxyzmm[3] = zoom;
797
+ const mm = this.frac2mm([0.5, 0.5, 0.5])
798
+ this.scene.pan2Dxyzmm[0] += zoomChange * mm[0]
799
+ this.scene.pan2Dxyzmm[1] += zoomChange * mm[1]
800
+ this.scene.pan2Dxyzmm[2] += zoomChange * mm[2]
801
+ this.drawScene()
802
+ }
803
+
804
+
805
+ Niivue.prototype.resetZoom = function () {
806
+ // this.scene.pan2Dxyzmm[0] = 0;
807
+ // this.scene.pan2Dxyzmm[1] = 0;
808
+ // this.scene.pan2Dxyzmm[2] = 0;
809
+
810
+ const zoom = 1;
811
+
812
+ const zoomChange = this.scene.pan2Dxyzmm[3] - zoom
813
+ this.scene.pan2Dxyzmm[3] = zoom;
814
+ const mm = this.frac2mm(this.scene.crosshairPos)
815
+ this.scene.pan2Dxyzmm[0] += zoomChange * mm[0]
816
+ this.scene.pan2Dxyzmm[1] += zoomChange * mm[1]
817
+ this.scene.pan2Dxyzmm[2] += zoomChange * mm[2]
818
+ this.drawScene()
819
+ }
820
+
821
+ Niivue.prototype.setCenteredZoom = function (zoom) {
822
+ this.scene.pan2Dxyzmm[0] = 0;
823
+ this.scene.pan2Dxyzmm[1] = 0;
824
+ this.scene.pan2Dxyzmm[2] = 0;
825
+
826
+ const zoomChange = this.scene.pan2Dxyzmm[3] - zoom
827
+ this.scene.pan2Dxyzmm[3] = zoom;
828
+ const mm = this.frac2mm(this.scene.crosshairPos)
829
+ this.scene.pan2Dxyzmm[0] += zoomChange * mm[0]
830
+ this.scene.pan2Dxyzmm[1] += zoomChange * mm[1]
831
+ this.scene.pan2Dxyzmm[2] += zoomChange * mm[2]
832
+ this.drawScene()
833
+ }
834
+
835
+ Niivue.prototype.resetContrast = function () {
836
+ this.volumes[0].cal_min = this.volumes[0].robust_min;
837
+ this.volumes[0].cal_max = this.volumes[0].robust_max;
838
+ this.onIntensityChange(this.volumes[0]);
839
+ this.refreshLayers(this.volumes[0], 0);
840
+ this.drawScene();
841
+ if (this.onResetContrast)
842
+ this.onResetContrast();
843
+ }
844
+
845
+ Niivue.prototype.relabelROIs = function (source = 0, target = 0) {
846
+ clearBitmapOverlay();
847
+ for (let i = 0; i < this.drawBitmap.length; i++) {
848
+ if (this.drawBitmap[i] === source) {
849
+ this.drawBitmap[i] = target;
850
+ }
851
+ }
852
+ this.refreshDrawing(true);
853
+ }
854
+
855
+ Niivue.prototype.loadDrawingFromBase64 = async function (fnm, base64) {
856
+ if (this.drawBitmap) {
857
+ console.debug('Overwriting open drawing!')
858
+ }
859
+ this.drawClearAllUndoBitmaps()
860
+ clearBitmapOverlay()
861
+ try {
862
+ // const volume = await NVImage.loadFromUrl()
863
+ if (base64) {
864
+ let imageOptions = NVImageFromUrlOptions(fnm);
865
+ const drawingBitmap = NVImage.loadFromBase64({ name: fnm, base64 })
866
+ if (drawingBitmap) {
867
+ this.loadDrawing(drawingBitmap)
868
+ }
869
+ }
870
+ } catch (err) {
871
+ console.error(err);
872
+ console.error('loadDrawingFromBlob() failed to load ' + fnm)
873
+ this.drawClearAllUndoBitmaps()
874
+ clearBitmapOverlay()
875
+ }
876
+ return base64 !== undefined;
877
+ }
878
+
879
+ /**
880
+ * Merge an uploaded/loaded drawing into the current drawBitmap instead of replacing it.
881
+ * Non-zero import voxels overwrite the current bitmap (same as drawing a new ROI on top).
882
+ * Import label IDs are reused when that ID is not already on the canvas (same color as when drawn);
883
+ * otherwise they are remapped to the next free ID to avoid collisions.
884
+ * @returns {{ ok: boolean, importLabelRemap: Record<number, number>|null }}
885
+ * importLabelRemap null means the full scene was loaded with loadDrawing (no prior drawing).
886
+ */
887
+ Niivue.prototype.mergeDrawingFromBase64 = async function (fnm, base64) {
888
+ const result = { ok: false, importLabelRemap: null };
889
+ if (!base64) {
890
+ return result;
891
+ }
892
+ try {
893
+ if (!this.back) {
894
+ throw new Error('back undefined');
895
+ }
896
+ const drawingBitmap = NVImage.loadFromBase64({ name: fnm, base64 });
897
+ if (!drawingBitmap || !drawingBitmap.hdr || !drawingBitmap.hdr.dims) {
898
+ return result;
899
+ }
900
+ const dims = drawingBitmap.hdr.dims;
901
+ if (
902
+ dims[1] !== this.back.hdr.dims[1] ||
903
+ dims[2] !== this.back.hdr.dims[2] ||
904
+ dims[3] !== this.back.hdr.dims[3]
905
+ ) {
906
+ console.warn('mergeDrawingFromBase64: drawing dimensions do not match background');
907
+ return result;
908
+ }
909
+ const vx = dims[1] * dims[2] * dims[3];
910
+ const perm = drawingBitmap.permRAS;
911
+ const layout = [0, 0, 0];
912
+ for (let i = 0; i < 3; i++) {
913
+ for (let j = 0; j < 3; j++) {
914
+ if (Math.abs(perm[i]) - 1 !== j) {
915
+ continue;
916
+ }
917
+ layout[j] = i * Math.sign(perm[i]);
918
+ }
919
+ }
920
+ let stride = 1;
921
+ const instride = [1, 1, 1];
922
+ const inflip = [false, false, false];
923
+ for (let i = 0; i < layout.length; i++) {
924
+ for (let j = 0; j < layout.length; j++) {
925
+ const a = Math.abs(layout[j]);
926
+ if (a !== i) {
927
+ continue;
928
+ }
929
+ instride[j] = stride;
930
+ if (layout[j] < 0 || Object.is(layout[j], -0)) {
931
+ inflip[j] = true;
932
+ }
933
+ stride *= dims[j + 1];
934
+ }
935
+ }
936
+ const nvRange = (start, end, step) => {
937
+ const o = [];
938
+ if (step > 0) {
939
+ for (let i = start; i <= end; i += step) {
940
+ o.push(i);
941
+ }
942
+ } else {
943
+ for (let i = start; i >= end; i += step) {
944
+ o.push(i);
945
+ }
946
+ }
947
+ return o;
948
+ };
949
+ let xlut = nvRange(0, dims[1] - 1, 1);
950
+ if (inflip[0]) {
951
+ xlut = nvRange(dims[1] - 1, 0, -1);
952
+ }
953
+ for (let i = 0; i < dims[1]; i++) {
954
+ xlut[i] *= instride[0];
955
+ }
956
+ let ylut = nvRange(0, dims[2] - 1, 1);
957
+ if (inflip[1]) {
958
+ ylut = nvRange(dims[2] - 1, 0, -1);
959
+ }
960
+ for (let i = 0; i < dims[2]; i++) {
961
+ ylut[i] *= instride[1];
962
+ }
963
+ let zlut = nvRange(0, dims[3] - 1, 1);
964
+ if (inflip[2]) {
965
+ zlut = nvRange(dims[3] - 1, 0, -1);
966
+ }
967
+ for (let i = 0; i < dims[3]; i++) {
968
+ zlut[i] *= instride[2];
969
+ }
970
+
971
+ const inVs = drawingBitmap.img;
972
+ const importFlat = new Uint8Array(vx);
973
+ let ji = 0;
974
+ for (let z = 0; z < dims[3]; z++) {
975
+ for (let y = 0; y < dims[2]; y++) {
976
+ for (let x = 0; x < dims[1]; x++) {
977
+ importFlat[xlut[x] + ylut[y] + zlut[z]] = inVs[ji];
978
+ ji++;
979
+ }
980
+ }
981
+ }
982
+
983
+ const hasExistingDrawing =
984
+ this.drawBitmap &&
985
+ this.drawBitmap.length === vx &&
986
+ this.drawBitmap.some((v) => v !== 0);
987
+
988
+ if (this.drawBitmap && this.drawBitmap.length !== vx) {
989
+ console.warn('mergeDrawingFromBase64: drawBitmap size mismatch; replacing drawing');
990
+ clearBitmapOverlay();
991
+ this.loadDrawing(drawingBitmap);
992
+ result.ok = true;
993
+ result.importLabelRemap = null;
994
+ return result;
995
+ }
996
+
997
+ if (!hasExistingDrawing) {
998
+ clearBitmapOverlay();
999
+ this.loadDrawing(drawingBitmap);
1000
+ result.ok = true;
1001
+ result.importLabelRemap = null;
1002
+ return result;
1003
+ }
1004
+
1005
+ const usedOnCanvas = new Set();
1006
+ for (let i = 0; i < this.drawBitmap.length; i++) {
1007
+ const v = this.drawBitmap[i];
1008
+ if (v !== 0) {
1009
+ usedOnCanvas.add(v);
1010
+ }
1011
+ }
1012
+
1013
+ const seen = new Set();
1014
+ for (let i = 0; i < importFlat.length; i++) {
1015
+ const v = importFlat[i];
1016
+ if (v !== 0) {
1017
+ seen.add(v);
1018
+ }
1019
+ }
1020
+ const sorted = Array.from(seen).sort((a, b) => a - b);
1021
+ const importLabelRemap = {};
1022
+ let nextSynthetic = 0;
1023
+ for (const v of usedOnCanvas) {
1024
+ if (v > nextSynthetic) {
1025
+ nextSynthetic = v;
1026
+ }
1027
+ }
1028
+ nextSynthetic += 1;
1029
+
1030
+ for (let s = 0; s < sorted.length; s++) {
1031
+ const oldL = sorted[s];
1032
+ if (!usedOnCanvas.has(oldL)) {
1033
+ importLabelRemap[oldL] = oldL;
1034
+ usedOnCanvas.add(oldL);
1035
+ } else {
1036
+ while (usedOnCanvas.has(nextSynthetic)) {
1037
+ nextSynthetic++;
1038
+ }
1039
+ importLabelRemap[oldL] = nextSynthetic;
1040
+ usedOnCanvas.add(nextSynthetic);
1041
+ nextSynthetic++;
1042
+ }
1043
+ }
1044
+
1045
+ const importWrittenVoxels = new Set();
1046
+ for (let i = 0; i < vx; i++) {
1047
+ if (importFlat[i] !== 0) {
1048
+ importWrittenVoxels.add(i);
1049
+ }
1050
+ }
1051
+ stripBitmapOverlayForVoxelIndices(importWrittenVoxels);
1052
+
1053
+ for (let i = 0; i < vx; i++) {
1054
+ const v = importFlat[i];
1055
+ if (v === 0) {
1056
+ continue;
1057
+ }
1058
+ // Upload wins overlaps (matches pen drawing on top of another ROI)
1059
+ this.drawBitmap[i] = importLabelRemap[v];
1060
+ }
1061
+
1062
+ this.drawAddUndoBitmap();
1063
+ this.refreshDrawing(false);
1064
+ this.drawScene();
1065
+ result.ok = true;
1066
+ result.importLabelRemap = importLabelRemap;
1067
+ return result;
1068
+ } catch (err) {
1069
+ console.error(err);
1070
+ console.error('mergeDrawingFromBase64() failed to load ' + fnm);
1071
+ return result;
1072
+ }
1073
+ };
1074
+
1075
+ // not included in public docs
1076
+ // show text labels for L/R, A/P, I/S dimensions
1077
+ Niivue.prototype.drawSliceOrientationText = function (leftTopWidthHeight, axCorSag) {
1078
+ if (this.hideText) {
1079
+ return;
1080
+ }
1081
+ this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height)
1082
+ let topText = 'S'
1083
+ if (axCorSag === SLICE_TYPE.AXIAL) {
1084
+ topText = 'A'
1085
+ }
1086
+ let leftText = this.opts.isRadiologicalConvention ? 'R' : 'L'
1087
+ if (axCorSag === SLICE_TYPE.SAGITTAL) {
1088
+ leftText = this.opts.sagittalNoseLeft ? 'A' : 'P'
1089
+ }
1090
+ if (this.opts.isCornerOrientationText) {
1091
+ this.drawTextRightBelow([leftTopWidthHeight[0], leftTopWidthHeight[1]], leftText + topText)
1092
+ return
1093
+ }
1094
+ this.drawTextBelow([leftTopWidthHeight[0] + leftTopWidthHeight[2] * 0.5, leftTopWidthHeight[1]], topText)
1095
+
1096
+ this.drawTextRight([leftTopWidthHeight[0], leftTopWidthHeight[1] + leftTopWidthHeight[3] * 0.5], leftText)
1097
+ }
1098
+
1099
+ // not included in public docs
1100
+ // draw line (can be diagonal)
1101
+ // unless Alpha is > 0, default color is opts.crosshairColor
1102
+ Niivue.prototype.drawLine = function (startXYendXY, thickness = 1, lineColor = [1, 0, 0, -1]) {
1103
+ console.log(startXYendXY);
1104
+ this.gl.bindVertexArray(this.genericVAO)
1105
+ if (!this.lineShader) {
1106
+ throw new Error('lineShader undefined')
1107
+ }
1108
+ this.lineShader.use(this.gl)
1109
+ if (lineColor[3] < 0) {
1110
+ lineColor = this.opts.crosshairColor
1111
+ }
1112
+ this.gl.uniform4fv(this.lineShader.uniforms.lineColor, lineColor)
1113
+ this.gl.uniform2fv(this.lineShader.uniforms.canvasWidthHeight, [this.gl.canvas.width, this.gl.canvas.height])
1114
+ // draw Line
1115
+ this.gl.uniform1f(this.lineShader.uniforms.thickness, thickness)
1116
+ this.gl.uniform4fv(this.lineShader.uniforms.startXYendXY, startXYendXY)
1117
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4)
1118
+ this.gl.bindVertexArray(this.unusedVAO) // set vertex attributes
1119
+ }
1120
+
1121
+ // not included in public docs
1122
+ // note: no test yet
1123
+ Niivue.prototype.calculateNewRange = function ({ volIdx = 0 } = {}) {
1124
+ if (this.opts.sliceType === SLICE_TYPE.RENDER && this.sliceMosaicString.length < 1) {
1125
+ return
1126
+ }
1127
+ if (this.uiData.dragStart[0] === this.uiData.dragEnd[0] && this.uiData.dragStart[1] === this.uiData.dragEnd[1]) {
1128
+ return
1129
+ }
1130
+ // calculate our box
1131
+ let frac = this.canvasPos2frac([this.uiData.dragStart[0], this.uiData.dragStart[1]])
1132
+ if (frac[0] < 0) {
1133
+ return
1134
+ }
1135
+ const startVox = this.frac2vox(frac, volIdx)
1136
+ frac = this.canvasPos2frac([this.uiData.dragEnd[0], this.uiData.dragEnd[1]])
1137
+ if (frac[0] < 0) {
1138
+ return
1139
+ }
1140
+ const endVox = this.frac2vox(frac, volIdx)
1141
+
1142
+ let hi = -Number.MAX_VALUE
1143
+ let lo = Number.MAX_VALUE
1144
+ const xrange = this.calculateMinMaxVoxIdx([startVox[0], endVox[0]])
1145
+ const yrange = this.calculateMinMaxVoxIdx([startVox[1], endVox[1]])
1146
+ const zrange = this.calculateMinMaxVoxIdx([startVox[2], endVox[2]])
1147
+
1148
+ // for our constant dimension we add one so that the for loop runs at least once
1149
+ if (startVox[0] - endVox[0] === 0) {
1150
+ xrange[1] = startVox[0] + 1
1151
+ } else if (startVox[1] - endVox[1] === 0) {
1152
+ yrange[1] = startVox[1] + 1
1153
+ } else if (startVox[2] - endVox[2] === 0) {
1154
+ zrange[1] = startVox[2] + 1
1155
+ }
1156
+
1157
+ const hdr = this.volumes[volIdx].hdr
1158
+ const img = this.volumes[volIdx].img
1159
+ if (!hdr || !img) {
1160
+ return
1161
+ }
1162
+
1163
+ const xdim = hdr.dims[1]
1164
+ const ydim = hdr.dims[2]
1165
+ for (let z = zrange[0]; z < zrange[1]; z++) {
1166
+ const zi = z * xdim * ydim
1167
+ for (let y = yrange[0]; y < yrange[1]; y++) {
1168
+ const yi = y * xdim
1169
+ for (let x = xrange[0]; x < xrange[1]; x++) {
1170
+ const index = zi + yi + x
1171
+ if (lo > img[index]) {
1172
+ lo = img[index]
1173
+ }
1174
+ if (hi < img[index]) {
1175
+ hi = img[index]
1176
+ }
1177
+ }
1178
+ }
1179
+ }
1180
+ if (lo >= hi) {
1181
+ return
1182
+ } // no variability or outside volume
1183
+ const mnScale = intensityRaw2Scaled(hdr, lo)
1184
+ const mxScale = intensityRaw2Scaled(hdr, hi)
1185
+ this.volumes[volIdx].cal_min = mnScale
1186
+ this.volumes[volIdx].cal_max = mxScale
1187
+ console.log(mnScale);
1188
+ console.log(mxScale);
1189
+ this.onIntensityChange(this.volumes[volIdx])
1190
+ }
1191
+
1192
+ function create3() {
1193
+ var out = new Float32Array();
1194
+ // if (ARRAY_TYPE != Float32Array) {
1195
+ // out[0] = 0;
1196
+ // out[1] = 0;
1197
+ // out[2] = 0;
1198
+ // }
1199
+ return out;
1200
+ }
1201
+
1202
+ function swizzleVec3(vec, order = [0, 1, 2]) {
1203
+ const vout = create3();
1204
+ vout[0] = vec[order[0]];
1205
+ vout[1] = vec[order[1]];
1206
+ vout[2] = vec[order[2]];
1207
+ return vout;
1208
+ }
1209
+
1210
+ Niivue.prototype.drawCrossLinesMM = function (sliceIndex, axCorSag, axiMM, corMM, sagMM) {
1211
+ console.log('method called');
1212
+ if (sliceIndex < 0 || this.screenSlices.length <= sliceIndex) {
1213
+ return;
1214
+ }
1215
+ const tile = this.screenSlices[sliceIndex];
1216
+ let sliceFrac = tile.sliceFrac;
1217
+ const isRender = sliceFrac === Infinity;
1218
+ if (isRender) {
1219
+ console.warn("Rendering approximate cross lines in world view mode");
1220
+ }
1221
+ if (sliceFrac === Infinity) {
1222
+ sliceFrac = 0.5;
1223
+ }
1224
+ let linesH = corMM.slice();
1225
+ let linesV = sagMM.slice();
1226
+ const thick = Math.max(7, this.opts.crosshairWidth);
1227
+ if (axCorSag === SLICE_TYPE.CORONAL) {
1228
+ linesH = axiMM.slice();
1229
+ }
1230
+ if (axCorSag === SLICE_TYPE.SAGITTAL) {
1231
+ linesH = axiMM.slice();
1232
+ linesV = corMM.slice();
1233
+ }
1234
+ function mm2screen(mm) {
1235
+ const screenXY = [0, 0];
1236
+ screenXY[0] = tile.leftTopWidthHeight[0] + (mm[0] - tile.leftTopMM[0]) / tile.fovMM[0] * tile.leftTopWidthHeight[2];
1237
+ screenXY[1] = tile.leftTopWidthHeight[1] + tile.leftTopWidthHeight[3] - (mm[1] - tile.leftTopMM[1]) / tile.fovMM[1] * tile.leftTopWidthHeight[3];
1238
+ return screenXY;
1239
+ }
1240
+ if (linesH.length > 0 && axCorSag === 0) {
1241
+ const fracZ = sliceFrac;
1242
+ const dimV = 1;
1243
+ for (let i2 = 0; i2 < linesH.length; i2++) {
1244
+ const mmV = this.frac2mm([0.5, 0.5, 0.5]);
1245
+ mmV[dimV] = linesH[i2];
1246
+ let fracY = this.mm2frac(mmV);
1247
+ fracY = fracY[dimV];
1248
+ let left = this.frac2mm([0, fracY, fracZ]);
1249
+ left = swizzleVec3(left, [0, 1, 2]);
1250
+ let right = this.frac2mm([1, fracY, fracZ]);
1251
+ right = swizzleVec3(right, [0, 1, 2]);
1252
+ left = mm2screen(left);
1253
+ right = mm2screen(right);
1254
+ this.drawLine([left[0], left[1], right[0], right[1]], thick);
1255
+ }
1256
+ }
1257
+ if (linesH.length > 0 && axCorSag === 1) {
1258
+ const fracH = sliceFrac;
1259
+ const dimV = 2;
1260
+ for (let i2 = 0; i2 < linesH.length; i2++) {
1261
+ const mmV = this.frac2mm([0.5, 0.5, 0.5]);
1262
+ mmV[dimV] = linesH[i2];
1263
+ let fracV = this.mm2frac(mmV);
1264
+ fracV = fracV[dimV];
1265
+ let left = this.frac2mm([0, fracH, fracV]);
1266
+ left = swizzleVec3(left, [0, 2, 1]);
1267
+ let right = this.frac2mm([1, fracH, fracV]);
1268
+ right = swizzleVec3(right, [0, 2, 1]);
1269
+ left = mm2screen(left);
1270
+ right = mm2screen(right);
1271
+ this.drawLine([left[0], left[1], right[0], right[1]], thick);
1272
+ }
1273
+ }
1274
+ if (linesH.length > 0 && axCorSag === 2) {
1275
+ const fracX = sliceFrac;
1276
+ const dimV = 2;
1277
+ for (let i2 = 0; i2 < linesH.length; i2++) {
1278
+ const mmV = this.frac2mm([0.5, 0.5, 0.5]);
1279
+ mmV[dimV] = linesH[i2];
1280
+ let fracZ = this.mm2frac(mmV);
1281
+ fracZ = fracZ[dimV];
1282
+ let left = this.frac2mm([fracX, 0, fracZ]);
1283
+ left = swizzleVec3(left, [1, 2, 0]);
1284
+ let right = this.frac2mm([fracX, 1, fracZ]);
1285
+ right = swizzleVec3(right, [1, 2, 0]);
1286
+ left = mm2screen(left);
1287
+ right = mm2screen(right);
1288
+ this.drawLine([left[0], left[1], right[0], right[1]], thick);
1289
+ }
1290
+ }
1291
+ if (linesV.length > 0 && axCorSag === 0) {
1292
+ const fracZ = sliceFrac;
1293
+ const dimH = 0;
1294
+ for (let i2 = 0; i2 < linesV.length; i2++) {
1295
+ const mm = this.frac2mm([0.5, 0.5, 0.5]);
1296
+ mm[dimH] = linesV[i2];
1297
+ let frac = this.mm2frac(mm);
1298
+ frac = frac[dimH];
1299
+ let left = this.frac2mm([frac, 0, fracZ]);
1300
+ left = swizzleVec3(left, [0, 1, 2]);
1301
+ let right = this.frac2mm([frac, 1, fracZ]);
1302
+ right = swizzleVec3(right, [0, 1, 2]);
1303
+ left = mm2screen(left);
1304
+ right = mm2screen(right);
1305
+ this.drawLine([left[0], left[1], right[0], right[1]], thick);
1306
+ }
1307
+ }
1308
+ if (linesV.length > 0 && axCorSag === 1) {
1309
+ const fracY = sliceFrac;
1310
+ const dimH = 0;
1311
+ for (let i2 = 0; i2 < linesV.length; i2++) {
1312
+ const mm = this.frac2mm([0.5, 0.5, 0.5]);
1313
+ mm[dimH] = linesV[i2];
1314
+ let frac = this.mm2frac(mm);
1315
+ frac = frac[dimH];
1316
+ let left = this.frac2mm([frac, fracY, 0]);
1317
+ left = swizzleVec3(left, [0, 2, 1]);
1318
+ let right = this.frac2mm([frac, fracY, 1]);
1319
+ right = swizzleVec3(right, [0, 2, 1]);
1320
+ left = mm2screen(left);
1321
+ right = mm2screen(right);
1322
+ this.drawLine([left[0], left[1], right[0], right[1]], thick);
1323
+ }
1324
+ }
1325
+ if (linesV.length > 0 && axCorSag === 2) {
1326
+ const fracX = sliceFrac;
1327
+ const dimH = 1;
1328
+ for (let i2 = 0; i2 < linesV.length; i2++) {
1329
+ const mm = this.frac2mm([0.5, 0.5, 0.5]);
1330
+ mm[dimH] = linesV[i2];
1331
+ let frac = this.mm2frac(mm);
1332
+ frac = frac[dimH];
1333
+ let left = this.frac2mm([fracX, frac, 0]);
1334
+ left = swizzleVec3(left, [1, 2, 0]);
1335
+ let right = this.frac2mm([fracX, frac, 1]);
1336
+ right = swizzleVec3(right, [1, 2, 0]);
1337
+ left = mm2screen(left);
1338
+ right = mm2screen(right);
1339
+ this.drawLine([left[0], left[1], right[0], right[1]], thick);
1340
+ }
1341
+ }
1342
+ }
1343
+
1344
+
1345
+ // not included in public docs
1346
+ // Niivue.prototype.drawCrosshairs3D=function(isDepthTest = true, alpha = 1, mvpMtx = null, is2DView = false, isSliceMM = true) {
1347
+ // console.log('method called');
1348
+ // if (!this.opts.show3Dcrosshair && !is2DView) {
1349
+ // return;
1350
+ // }
1351
+ // if (this.opts.crosshairWidth <= 0 && is2DView) {
1352
+ // return;
1353
+ // }
1354
+ // const gl = this.gl;
1355
+ // const mm = this.frac2mm(this.scene.crosshairPos, 0, isSliceMM);
1356
+ // if (this.crosshairs3D === null || this.crosshairs3D.mm[0] !== mm[0] || this.crosshairs3D.mm[1] !== mm[1] || this.crosshairs3D.mm[2] !== mm[2]) {
1357
+ // if (this.crosshairs3D !== null) {
1358
+ // gl.deleteBuffer(this.crosshairs3D.indexBuffer);
1359
+ // gl.deleteBuffer(this.crosshairs3D.vertexBuffer);
1360
+ // }
1361
+ // const [mn, mx, range] = this.sceneExtentsMinMax(isSliceMM);
1362
+ // let radius = 1;
1363
+ // if (this.volumes.length > 0) {
1364
+ // radius = 0.5 * Math.min(Math.min(this.back.pixDims[1], this.back.pixDims[2]), this.back.pixDims[3]);
1365
+ // } else if (range[0] < 50 || range[0] > 1e3) {
1366
+ // radius = range[0] * 0.02;
1367
+ // }
1368
+ // radius *= this.opts.crosshairWidth;
1369
+ // this.crosshairs3D = NiivueObject3D.generateCrosshairs(this.gl, 1, mm, mn, mx, radius);
1370
+ // this.crosshairs3D.mm = mm;
1371
+ // }
1372
+ // const crosshairsShader = this.surfaceShader;
1373
+ // crosshairsShader.use(this.gl);
1374
+ // if (mvpMtx === null) {
1375
+ // ;
1376
+ // [mvpMtx] = this.calculateMvpMatrix(this.crosshairs3D, this.scene.renderAzimuth, this.scene.renderElevation);
1377
+ // }
1378
+ // gl.uniformMatrix4fv(crosshairsShader.mvpLoc, false, mvpMtx);
1379
+ // gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.crosshairs3D.indexBuffer);
1380
+ // gl.enable(gl.DEPTH_TEST);
1381
+ // const color = [...this.opts.crosshairColor];
1382
+ // if (isDepthTest) {
1383
+ // gl.disable(gl.BLEND);
1384
+ // gl.depthFunc(gl.GREATER);
1385
+ // } else {
1386
+ // gl.enable(gl.BLEND);
1387
+ // gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
1388
+ // gl.depthFunc(gl.ALWAYS);
1389
+ // }
1390
+ // color[3] = alpha;
1391
+ // gl.uniform4fv(crosshairsShader.colorLoc, color);
1392
+ // gl.bindVertexArray(this.crosshairs3D.vao);
1393
+ // gl.drawElements(
1394
+ // gl.TRIANGLES,
1395
+ // this.crosshairs3D.indexCount,
1396
+ // gl.UNSIGNED_INT,
1397
+ // // gl.UNSIGNED_SHORT,
1398
+ // 0
1399
+ // );
1400
+ // gl.bindVertexArray(this.unusedVAO);
1401
+ // }
1402
+ export { Niivue };