cloudmr-ux 4.3.7 → 4.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CmrComponents/niivue-contrast-adjustments/NiivueContrastAdjustments.d.ts +4 -3
- package/dist/CmrComponents/niivue-contrast-adjustments/NiivueContrastAdjustments.js +5 -5
- package/dist/CmrComponents/niivue-slice-position/NiivueSlicePosition.js +2 -2
- package/dist/CmrComponents/niivue-viewer/CloudMrNiivuePanel.d.ts +38 -0
- package/dist/CmrComponents/niivue-viewer/CloudMrNiivuePanel.js +197 -0
- package/dist/CmrComponents/niivue-viewer/CloudMrNiivueViewer.d.ts +41 -0
- package/dist/CmrComponents/niivue-viewer/CloudMrNiivueViewer.js +1239 -0
- package/dist/CmrComponents/niivue-viewer/ColorPicker.d.ts +1 -0
- package/dist/CmrComponents/niivue-viewer/ColorPicker.js +65 -0
- package/dist/CmrComponents/niivue-viewer/Layer.d.ts +1 -0
- package/dist/CmrComponents/niivue-viewer/Layer.js +122 -0
- package/dist/CmrComponents/niivue-viewer/LayersPanel.d.ts +1 -0
- package/dist/CmrComponents/niivue-viewer/LayersPanel.js +107 -0
- package/dist/CmrComponents/niivue-viewer/Niivue.css +8 -0
- package/dist/CmrComponents/niivue-viewer/NiivuePatcher.d.ts +2 -0
- package/dist/CmrComponents/niivue-viewer/NiivuePatcher.js +1402 -0
- package/dist/CmrComponents/niivue-viewer/NumberPicker.d.ts +1 -0
- package/dist/CmrComponents/niivue-viewer/NumberPicker.js +40 -0
- package/dist/CmrComponents/niivue-viewer/SettingsPanel.d.ts +1 -0
- package/dist/CmrComponents/niivue-viewer/SettingsPanel.js +30 -0
- package/dist/CmrComponents/niivue-viewer/Switch.d.ts +1 -0
- package/dist/CmrComponents/niivue-viewer/Switch.js +27 -0
- package/dist/CmrComponents/niivue-viewer/Toolbar.d.ts +48 -0
- package/dist/CmrComponents/niivue-viewer/Toolbar.js +276 -0
- package/dist/CmrComponents/niivue-viewer/Toolbar.scss +40 -0
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/DrawColorPlatte.d.ts +2 -0
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/DrawColorPlatte.js +61 -0
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/EraserPlatte.d.ts +2 -0
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/EraserPlatte.js +56 -0
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/MaskPlatte.d.ts +2 -0
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/MaskPlatte.js +148 -0
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/MroDrawToolkit.d.ts +1 -0
- package/dist/CmrComponents/niivue-viewer/mro-draw-toolkit/MroDrawToolkit.js +177 -0
- package/dist/CmrComponents/niivue-viewer/niivuePenType.d.ts +10 -0
- package/dist/CmrComponents/niivue-viewer/niivuePenType.js +10 -0
- package/dist/core/common/components/NiivueTools/NiivuePatcher.d.ts +2 -0
- package/dist/core/common/components/NiivueTools/components/ControlThemes.d.ts +1 -0
- package/dist/core/common/components/NiivueTools/components/ControlThemes.js +123 -0
- package/dist/core/common/components/NiivueTools/components/Example.d.ts +10 -0
- package/dist/core/common/components/NiivueTools/components/Example.js +326 -0
- package/dist/core/common/components/NiivueTools/components/ImageList.d.ts +1 -0
- package/dist/core/common/components/NiivueTools/components/ImageList.js +22 -0
- package/dist/core/common/components/NiivueTools/components/ImageListItem.d.ts +1 -0
- package/dist/core/common/components/NiivueTools/components/ImageListItem.js +103 -0
- package/dist/core/common/components/NiivueTools/main.d.ts +1 -0
- package/dist/core/common/components/NiivueTools/main.js +16 -0
- package/dist/core/common/components/NiivueTools/util.d.ts +21 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- 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 };
|