@niivue/niivue 0.44.0 → 0.44.2-dev.1
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/README.md +9 -0
- package/build/index.d.ts +132 -18
- package/build/index.js +1088 -178
- package/build/index.js.map +1 -1
- package/build/index.min.js +1 -1
- package/dist/index.d.ts +132 -18
- package/dist/index.js +26585 -30810
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/niivue.umd.js +111 -32
- package/package.json +18 -17
- package/src/index.html +46 -7
- package/src/index_segment.html +165 -0
- package/src/index_sync.html +289 -0
- package/src/niivue/index.ts +857 -137
- package/src/nvdocument.ts +36 -5
- package/src/nvimage/index.ts +29 -3
- package/src/nvimage/utils.ts +61 -42
- package/src/nvlabel.ts +34 -1
- package/src/nvmesh-loaders.ts +50 -6
- package/src/nvmesh-utilities.ts +139 -0
- package/src/nvmesh.ts +96 -0
- package/src/shader-srcs.ts +84 -3
- package/src/types.ts +13 -0
package/src/nvdocument.ts
CHANGED
|
@@ -46,7 +46,8 @@ export enum DRAG_MODE {
|
|
|
46
46
|
measurement = 2,
|
|
47
47
|
pan = 3,
|
|
48
48
|
slicer3D = 4,
|
|
49
|
-
callbackOnly = 5
|
|
49
|
+
callbackOnly = 5,
|
|
50
|
+
roiSelection = 6
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
// make mutable type
|
|
@@ -96,6 +97,7 @@ export type NVConfigOptions = {
|
|
|
96
97
|
multiplanarPadPixels: number
|
|
97
98
|
// @deprecated
|
|
98
99
|
multiplanarForceRender: boolean
|
|
100
|
+
multiplanarEqualSize: boolean
|
|
99
101
|
multiplanarShowRender: SHOW_RENDER
|
|
100
102
|
isRadiologicalConvention: boolean
|
|
101
103
|
// string to allow infinity
|
|
@@ -139,10 +141,18 @@ export type NVConfigOptions = {
|
|
|
139
141
|
renderOverlayBlend: number
|
|
140
142
|
sliceMosaicString: string
|
|
141
143
|
centerMosaic: boolean
|
|
144
|
+
penSize: number
|
|
142
145
|
clickToSegment: boolean
|
|
143
146
|
clickToSegmentRadius: number
|
|
144
|
-
clickToSegmentSteps: number
|
|
145
147
|
clickToSegmentBright: boolean
|
|
148
|
+
clickToSegmentAutoIntensity: boolean // new option, but keep clickToSegmentBright for backwards compatibility
|
|
149
|
+
clickToSegmentIntensityMax: number // also covers NaN
|
|
150
|
+
clickToSegmentIntensityMin: number // also covers NaN
|
|
151
|
+
clickToSegmentPercent: number
|
|
152
|
+
clickToSegmentMaxDistanceMM: number // max distance in mm to consider for click to segment flood fill
|
|
153
|
+
// selection box outline thickness
|
|
154
|
+
selectionBoxLineThickness: number
|
|
155
|
+
selectionBoxIsOutline: boolean
|
|
146
156
|
}
|
|
147
157
|
|
|
148
158
|
export const DEFAULT_OPTIONS: NVConfigOptions = {
|
|
@@ -177,6 +187,7 @@ export const DEFAULT_OPTIONS: NVConfigOptions = {
|
|
|
177
187
|
multiplanarPadPixels: 0,
|
|
178
188
|
// @deprecated
|
|
179
189
|
multiplanarForceRender: false,
|
|
190
|
+
multiplanarEqualSize: false,
|
|
180
191
|
multiplanarShowRender: SHOW_RENDER.AUTO, // auto is the same behaviour as multiplanarForceRender: false
|
|
181
192
|
isRadiologicalConvention: false,
|
|
182
193
|
meshThicknessOn2D: Infinity,
|
|
@@ -210,13 +221,25 @@ export const DEFAULT_OPTIONS: NVConfigOptions = {
|
|
|
210
221
|
renderOverlayBlend: 1.0,
|
|
211
222
|
sliceMosaicString: '',
|
|
212
223
|
centerMosaic: false,
|
|
224
|
+
penSize: 1, // in voxels, since all drawing is done using bitmap indices
|
|
213
225
|
clickToSegment: false,
|
|
214
|
-
clickToSegmentRadius:
|
|
215
|
-
|
|
216
|
-
|
|
226
|
+
clickToSegmentRadius: 3, // in mm
|
|
227
|
+
clickToSegmentBright: true,
|
|
228
|
+
clickToSegmentAutoIntensity: false, // new option, but keep clickToSegmentBright for backwards compatibility
|
|
229
|
+
clickToSegmentIntensityMax: NaN, // NaN will use auto threshold (default flood fill behavior from before)
|
|
230
|
+
clickToSegmentIntensityMin: NaN, // NaN will use auto threshold (default flood fill behavior from before)
|
|
231
|
+
// 0 will use auto threshold (default flood fill behavior from before)
|
|
232
|
+
// Take the voxel intensity at the click point and use this percentage +/- to threshold the flood fill operation.
|
|
233
|
+
// If greater than 0, clickedVoxelIntensity +/- clickedVoxelIntensity * clickToSegmentPercent will be used
|
|
234
|
+
// for the clickToSegmentIntensityMin and clickToSegmentIntensityMax values.
|
|
235
|
+
clickToSegmentPercent: 0,
|
|
236
|
+
clickToSegmentMaxDistanceMM: Number.POSITIVE_INFINITY, // default value is infinity for backwards compatibility with flood fill routine.
|
|
237
|
+
selectionBoxLineThickness: 4,
|
|
238
|
+
selectionBoxIsOutline: false
|
|
217
239
|
}
|
|
218
240
|
|
|
219
241
|
type SceneData = {
|
|
242
|
+
gamma: number
|
|
220
243
|
azimuth: number
|
|
221
244
|
elevation: number
|
|
222
245
|
crosshairPos: vec3
|
|
@@ -230,6 +253,7 @@ type SceneData = {
|
|
|
230
253
|
}
|
|
231
254
|
|
|
232
255
|
export const INITIAL_SCENE_DATA = {
|
|
256
|
+
gamma: 1.0,
|
|
233
257
|
azimuth: 110,
|
|
234
258
|
elevation: 10,
|
|
235
259
|
crosshairPos: vec3.create(),
|
|
@@ -255,6 +279,7 @@ export type Scene = {
|
|
|
255
279
|
pan2Dxyzmm: vec4
|
|
256
280
|
_elevation?: number
|
|
257
281
|
_azimuth?: number
|
|
282
|
+
gamma?: number
|
|
258
283
|
}
|
|
259
284
|
|
|
260
285
|
export type DocumentData = {
|
|
@@ -566,6 +591,12 @@ export class NVDocument {
|
|
|
566
591
|
}
|
|
567
592
|
|
|
568
593
|
data.labels = [...this.data.labels]
|
|
594
|
+
|
|
595
|
+
// remove any handlers
|
|
596
|
+
for (const label of data.labels) {
|
|
597
|
+
delete label.onClick
|
|
598
|
+
}
|
|
599
|
+
|
|
569
600
|
data.customData = this.customData
|
|
570
601
|
|
|
571
602
|
// volumes
|
package/src/nvimage/index.ts
CHANGED
|
@@ -1631,7 +1631,7 @@ export class NVImage {
|
|
|
1631
1631
|
hdr.datatypeCode = NiiDataType.DT_FLOAT64
|
|
1632
1632
|
break
|
|
1633
1633
|
default:
|
|
1634
|
-
throw new Error('Unsupported
|
|
1634
|
+
throw new Error('Unsupported MHA data type: ' + items[0])
|
|
1635
1635
|
}
|
|
1636
1636
|
}
|
|
1637
1637
|
if (line.startsWith('ObjectType') && !items[0].includes('Image')) {
|
|
@@ -1653,6 +1653,9 @@ export class NVImage {
|
|
|
1653
1653
|
[mat33[2], mat33[5], mat33[8], offset[2]],
|
|
1654
1654
|
[0, 0, 0, 1]
|
|
1655
1655
|
]
|
|
1656
|
+
while (bytes[pos] === 10) {
|
|
1657
|
+
pos++
|
|
1658
|
+
}
|
|
1656
1659
|
hdr.vox_offset = pos
|
|
1657
1660
|
if (isDetached && pairedImgData) {
|
|
1658
1661
|
if (isGz) {
|
|
@@ -3369,7 +3372,7 @@ export class NVImage {
|
|
|
3369
3372
|
* read a 3D slab of voxels from a volume
|
|
3370
3373
|
* @param voxStart - first row, column and slice (RAS order) for selection
|
|
3371
3374
|
* @param voxEnd - final row, column and slice (RAS order) for selection
|
|
3372
|
-
* @param dataType - array data type. Options: 'same' (default), 'uint8', 'float32'
|
|
3375
|
+
* @param dataType - array data type. Options: 'same' (default), 'uint8', 'float32', 'scaled', 'normalized', 'windowed'
|
|
3373
3376
|
* @returns the an array where ret[0] is the voxel values and ret[1] is dimension of selection
|
|
3374
3377
|
* @see {@link https://niivue.github.io/niivue/features/slab_selection.html | live demo usage}
|
|
3375
3378
|
*/
|
|
@@ -3394,7 +3397,12 @@ export class NVImage {
|
|
|
3394
3397
|
let dt = this.hdr!.datatypeCode
|
|
3395
3398
|
if (dataType === 'uint8') {
|
|
3396
3399
|
dt = NiiDataType.DT_UINT8
|
|
3397
|
-
} else if (
|
|
3400
|
+
} else if (
|
|
3401
|
+
dataType === 'float32' ||
|
|
3402
|
+
dataType === 'scaled' ||
|
|
3403
|
+
dataType === 'normalized' ||
|
|
3404
|
+
dataType === 'windowed'
|
|
3405
|
+
) {
|
|
3398
3406
|
dt = NiiDataType.DT_FLOAT32
|
|
3399
3407
|
}
|
|
3400
3408
|
if (dt === NiiDataType.DT_UINT8) {
|
|
@@ -3424,6 +3432,24 @@ export class NVImage {
|
|
|
3424
3432
|
}
|
|
3425
3433
|
}
|
|
3426
3434
|
}
|
|
3435
|
+
if (dataType === 'scaled' || dataType === 'normalized' || dataType === 'windowed') {
|
|
3436
|
+
for (let i = 0; i < img.length; i++) {
|
|
3437
|
+
img[i] = img[i] * this.hdr.scl_slope + this.hdr.scl_inter
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
if (dataType === 'normalized' || dataType === 'windowed') {
|
|
3441
|
+
let mn = this.cal_min
|
|
3442
|
+
let mx = this.cal_max
|
|
3443
|
+
if (dataType === 'normalized') {
|
|
3444
|
+
mn = this.global_min
|
|
3445
|
+
mx = this.global_max
|
|
3446
|
+
}
|
|
3447
|
+
const scale = 1 / (mx - mn)
|
|
3448
|
+
for (let i = 0; i < img.length; i++) {
|
|
3449
|
+
img[i] = (img[i] - mn) * scale
|
|
3450
|
+
img[i] = Math.max(Math.min(img[i], 1), 0)
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3427
3453
|
return [img, slabDims]
|
|
3428
3454
|
} // getVolumeData()
|
|
3429
3455
|
|
package/src/nvimage/utils.ts
CHANGED
|
@@ -373,10 +373,13 @@ following conditions are met:
|
|
|
373
373
|
return m
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
-
function str2Buffer(str: string): number[] {
|
|
376
|
+
function str2Buffer(str: string, maxLen: number = 80): number[] {
|
|
377
377
|
// emulate node.js Buffer.from
|
|
378
|
+
// remove characters than could be used for shell expansion
|
|
379
|
+
str = str.replace(/[`$]/g, '')
|
|
378
380
|
const bytes = []
|
|
379
|
-
|
|
381
|
+
const len = Math.min(maxLen, str.length)
|
|
382
|
+
for (let i = 0; i < len; i++) {
|
|
380
383
|
const char = str.charCodeAt(i)
|
|
381
384
|
bytes.push(char & 0xff)
|
|
382
385
|
}
|
|
@@ -384,70 +387,79 @@ function str2Buffer(str: string): number[] {
|
|
|
384
387
|
}
|
|
385
388
|
|
|
386
389
|
// save NIfTI header into UINT8 array for saving to disk
|
|
387
|
-
export function hdrToArrayBuffer(hdr: NiftiHeader, isDrawing8 = false): Uint8Array {
|
|
390
|
+
export function hdrToArrayBuffer(hdr: NiftiHeader, isDrawing8 = false, isInputEndian = false): Uint8Array {
|
|
388
391
|
const SHORT_SIZE = 2
|
|
389
392
|
const FLOAT32_SIZE = 4
|
|
390
|
-
|
|
393
|
+
let isLittleEndian = true
|
|
394
|
+
if (isInputEndian) {
|
|
395
|
+
isLittleEndian = hdr.littleEndian
|
|
396
|
+
}
|
|
391
397
|
const byteArray = new Uint8Array(348)
|
|
392
398
|
const view = new DataView(byteArray.buffer)
|
|
393
399
|
// sizeof_hdr
|
|
394
|
-
view.setInt32(0, 348,
|
|
400
|
+
view.setInt32(0, 348, isLittleEndian)
|
|
395
401
|
|
|
396
402
|
// data_type, db_name, extents, session_error, regular are not used
|
|
397
|
-
|
|
403
|
+
// regular set to 'r' (ASCII 114) for Analyze compatibility
|
|
404
|
+
view.setUint8(38, 114)
|
|
398
405
|
// dim_info
|
|
399
406
|
view.setUint8(39, hdr.dim_info)
|
|
400
407
|
|
|
401
408
|
// dims
|
|
402
409
|
for (let i = 0; i < 8; i++) {
|
|
403
|
-
view.setUint16(40 + SHORT_SIZE * i, hdr.dims[i],
|
|
410
|
+
view.setUint16(40 + SHORT_SIZE * i, hdr.dims[i], isLittleEndian)
|
|
404
411
|
}
|
|
405
412
|
|
|
406
413
|
// intent_p1, intent_p2, intent_p3
|
|
407
|
-
view.setFloat32(56, hdr.intent_p1,
|
|
408
|
-
view.setFloat32(60, hdr.intent_p2,
|
|
409
|
-
view.setFloat32(64, hdr.intent_p3,
|
|
414
|
+
view.setFloat32(56, hdr.intent_p1, isLittleEndian)
|
|
415
|
+
view.setFloat32(60, hdr.intent_p2, isLittleEndian)
|
|
416
|
+
view.setFloat32(64, hdr.intent_p3, isLittleEndian)
|
|
410
417
|
// intent_code, datatype, bitpix, slice_start
|
|
411
|
-
view.setInt16(68, hdr.intent_code,
|
|
418
|
+
view.setInt16(68, hdr.intent_code, isLittleEndian)
|
|
412
419
|
if (isDrawing8) {
|
|
413
|
-
view.setInt16(70, 2,
|
|
414
|
-
view.setInt16(72, 8,
|
|
420
|
+
view.setInt16(70, 2, isLittleEndian) // 2 = DT_UINT8
|
|
421
|
+
view.setInt16(72, 8, isLittleEndian)
|
|
415
422
|
} else {
|
|
416
|
-
view.setInt16(70, hdr.datatypeCode,
|
|
417
|
-
view.setInt16(72, hdr.numBitsPerVoxel,
|
|
423
|
+
view.setInt16(70, hdr.datatypeCode, isLittleEndian)
|
|
424
|
+
view.setInt16(72, hdr.numBitsPerVoxel, isLittleEndian)
|
|
418
425
|
}
|
|
419
|
-
view.setInt16(74, hdr.slice_start,
|
|
426
|
+
view.setInt16(74, hdr.slice_start, isLittleEndian)
|
|
420
427
|
|
|
421
428
|
// pixdim[8], vox_offset, scl_slope, scl_inter
|
|
422
429
|
for (let i = 0; i < 8; i++) {
|
|
423
|
-
view.setFloat32(76 + FLOAT32_SIZE * i, hdr.pixDims[i],
|
|
430
|
+
view.setFloat32(76 + FLOAT32_SIZE * i, hdr.pixDims[i], isLittleEndian)
|
|
424
431
|
}
|
|
425
432
|
if (isDrawing8) {
|
|
426
|
-
view.setFloat32(108, 352,
|
|
427
|
-
view.setFloat32(112, 1.0,
|
|
428
|
-
view.setFloat32(116, 0.0,
|
|
433
|
+
view.setFloat32(108, 352, isLittleEndian)
|
|
434
|
+
view.setFloat32(112, 1.0, isLittleEndian)
|
|
435
|
+
view.setFloat32(116, 0.0, isLittleEndian)
|
|
429
436
|
} else {
|
|
430
|
-
view.setFloat32(108, hdr.vox_offset,
|
|
431
|
-
view.setFloat32(
|
|
432
|
-
view.setFloat32(
|
|
437
|
+
// view.setFloat32(108, hdr.vox_offset, isLittleEndian)
|
|
438
|
+
view.setFloat32(108, 352, isLittleEndian)
|
|
439
|
+
view.setFloat32(112, hdr.scl_slope, isLittleEndian)
|
|
440
|
+
view.setFloat32(116, hdr.scl_inter, isLittleEndian)
|
|
433
441
|
}
|
|
434
442
|
// slice_end
|
|
435
|
-
view.setInt16(120, hdr.slice_end,
|
|
443
|
+
view.setInt16(120, hdr.slice_end, isLittleEndian)
|
|
436
444
|
|
|
437
445
|
// slice_code, xyzt_units
|
|
438
446
|
view.setUint8(122, hdr.slice_code)
|
|
439
|
-
|
|
447
|
+
if (hdr.xyzt_units === 0) {
|
|
448
|
+
view.setUint8(123, 10)
|
|
449
|
+
} else {
|
|
450
|
+
view.setUint8(123, hdr.xyzt_units)
|
|
451
|
+
}
|
|
440
452
|
|
|
441
453
|
// cal_max, cal_min, slice_duration, toffset
|
|
442
454
|
if (isDrawing8) {
|
|
443
|
-
view.setFloat32(124, 0,
|
|
444
|
-
view.setFloat32(128, 0,
|
|
455
|
+
view.setFloat32(124, 0, isLittleEndian)
|
|
456
|
+
view.setFloat32(128, 0, isLittleEndian)
|
|
445
457
|
} else {
|
|
446
|
-
view.setFloat32(124, hdr.cal_max,
|
|
447
|
-
view.setFloat32(128, hdr.cal_min,
|
|
458
|
+
view.setFloat32(124, hdr.cal_max, isLittleEndian)
|
|
459
|
+
view.setFloat32(128, hdr.cal_min, isLittleEndian)
|
|
448
460
|
}
|
|
449
|
-
view.setFloat32(132, hdr.slice_duration,
|
|
450
|
-
view.setFloat32(136, hdr.toffset,
|
|
461
|
+
view.setFloat32(132, hdr.slice_duration, isLittleEndian)
|
|
462
|
+
view.setFloat32(136, hdr.toffset, isLittleEndian)
|
|
451
463
|
|
|
452
464
|
// glmax, glmin are unused
|
|
453
465
|
|
|
@@ -457,27 +469,34 @@ export function hdrToArrayBuffer(hdr: NiftiHeader, isDrawing8 = false): Uint8Arr
|
|
|
457
469
|
// node.js: byteArray.set(Buffer.from(hdr.aux_file), 228);
|
|
458
470
|
byteArray.set(str2Buffer(hdr.aux_file), 228)
|
|
459
471
|
// qform_code, sform_code
|
|
460
|
-
view.setInt16(252, hdr.qform_code,
|
|
461
|
-
|
|
472
|
+
view.setInt16(252, hdr.qform_code, isLittleEndian)
|
|
473
|
+
// if sform unknown, assume NIFTI_XFORM_SCANNER_ANAT
|
|
474
|
+
if (hdr.sform_code < 1 || hdr.sform_code < 1) {
|
|
475
|
+
view.setInt16(254, 1, isLittleEndian)
|
|
476
|
+
} else {
|
|
477
|
+
view.setInt16(254, hdr.sform_code, isLittleEndian)
|
|
478
|
+
}
|
|
462
479
|
|
|
463
480
|
// quatern_b, quatern_c, quatern_d, qoffset_x, qoffset_y, qoffset_z, srow_x[4], srow_y[4], and srow_z[4]
|
|
464
|
-
view.setFloat32(256, hdr.quatern_b,
|
|
465
|
-
view.setFloat32(260, hdr.quatern_c,
|
|
466
|
-
view.setFloat32(264, hdr.quatern_d,
|
|
467
|
-
view.setFloat32(268, hdr.qoffset_x,
|
|
468
|
-
view.setFloat32(272, hdr.qoffset_y,
|
|
469
|
-
view.setFloat32(276, hdr.qoffset_z,
|
|
481
|
+
view.setFloat32(256, hdr.quatern_b, isLittleEndian)
|
|
482
|
+
view.setFloat32(260, hdr.quatern_c, isLittleEndian)
|
|
483
|
+
view.setFloat32(264, hdr.quatern_d, isLittleEndian)
|
|
484
|
+
view.setFloat32(268, hdr.qoffset_x, isLittleEndian)
|
|
485
|
+
view.setFloat32(272, hdr.qoffset_y, isLittleEndian)
|
|
486
|
+
view.setFloat32(276, hdr.qoffset_z, isLittleEndian)
|
|
470
487
|
const flattened = hdr.affine.flat()
|
|
471
488
|
// we only want the first three rows
|
|
472
489
|
for (let i = 0; i < 12; i++) {
|
|
473
|
-
view.setFloat32(280 + FLOAT32_SIZE * i, flattened[i],
|
|
490
|
+
view.setFloat32(280 + FLOAT32_SIZE * i, flattened[i], isLittleEndian)
|
|
474
491
|
}
|
|
475
492
|
// node.js https://www.w3schools.com/nodejs/met_buffer_from.asp
|
|
476
493
|
// intent_name and magic
|
|
477
494
|
// node.js byteArray.set(Buffer.from(hdr.intent_name), 328);
|
|
478
|
-
byteArray.set(str2Buffer(hdr.intent_name), 328)
|
|
495
|
+
// byteArray.set(str2Buffer(hdr.intent_name), 328)
|
|
479
496
|
// node.js byteArray.set(Buffer.from(hdr.magic), 344);
|
|
480
|
-
byteArray.set(str2Buffer(hdr.magic), 344)
|
|
497
|
+
// byteArray.set(str2Buffer(hdr.magic), 344)
|
|
498
|
+
view.setInt32(344, 3222382, true) // "n+1\0"
|
|
499
|
+
|
|
481
500
|
return byteArray
|
|
482
501
|
// return byteArray.buffer;
|
|
483
502
|
}
|
package/src/nvlabel.ts
CHANGED
|
@@ -10,6 +10,29 @@ export enum LabelLineTerminator {
|
|
|
10
10
|
RING = 'ring'
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export enum LabelAnchorFlag {
|
|
14
|
+
NONE = 0,
|
|
15
|
+
LEFT = 1 << 0,
|
|
16
|
+
CENTER = 1 << 1,
|
|
17
|
+
RIGHT = 1 << 2,
|
|
18
|
+
TOP = 1 << 3,
|
|
19
|
+
MIDDLE = 1 << 4,
|
|
20
|
+
BOTTOM = 1 << 5
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export enum LabelAnchorPoint {
|
|
24
|
+
NONE = LabelAnchorFlag.NONE,
|
|
25
|
+
TOPLEFT = LabelAnchorFlag.TOP | LabelAnchorFlag.LEFT,
|
|
26
|
+
TOPCENTER = LabelAnchorFlag.TOP | LabelAnchorFlag.CENTER,
|
|
27
|
+
TOPRIGHT = LabelAnchorFlag.TOP | LabelAnchorFlag.RIGHT,
|
|
28
|
+
MIDDLELEFT = LabelAnchorFlag.MIDDLE | LabelAnchorFlag.LEFT,
|
|
29
|
+
MIDDLECENTER = LabelAnchorFlag.MIDDLE | LabelAnchorFlag.CENTER,
|
|
30
|
+
MIDDLERIGHT = LabelAnchorFlag.MIDDLE | LabelAnchorFlag.RIGHT,
|
|
31
|
+
BOTTOMLEFT = LabelAnchorFlag.BOTTOM | LabelAnchorFlag.LEFT,
|
|
32
|
+
BOTTOMCENTER = LabelAnchorFlag.BOTTOM | LabelAnchorFlag.CENTER,
|
|
33
|
+
BOTTOMRIGHT = LabelAnchorFlag.BOTTOM | LabelAnchorFlag.RIGHT
|
|
34
|
+
}
|
|
35
|
+
|
|
13
36
|
/**
|
|
14
37
|
* Class representing label style.
|
|
15
38
|
* @ignore
|
|
@@ -65,15 +88,25 @@ export class NVLabel3D {
|
|
|
65
88
|
text: string
|
|
66
89
|
style: NVLabel3DStyle
|
|
67
90
|
points?: number[] | number[][]
|
|
91
|
+
anchor: LabelAnchorPoint
|
|
92
|
+
onClick?: (label: NVLabel3D) => void
|
|
68
93
|
|
|
69
94
|
/**
|
|
70
95
|
* @param text - The text of the label
|
|
71
96
|
* @param style - The style of the label
|
|
72
97
|
* @param points - An array of points label for label lines
|
|
73
98
|
*/
|
|
74
|
-
constructor(
|
|
99
|
+
constructor(
|
|
100
|
+
text: string,
|
|
101
|
+
style: NVLabel3DStyle,
|
|
102
|
+
points?: number[] | number[][],
|
|
103
|
+
anchor?: LabelAnchorPoint,
|
|
104
|
+
onClick?: (label: NVLabel3D) => void
|
|
105
|
+
) {
|
|
75
106
|
this.text = text
|
|
76
107
|
this.style = style
|
|
77
108
|
this.points = points
|
|
109
|
+
this.anchor = anchor || LabelAnchorPoint.NONE
|
|
110
|
+
this.onClick = onClick
|
|
78
111
|
}
|
|
79
112
|
}
|
package/src/nvmesh-loaders.ts
CHANGED
|
@@ -709,6 +709,12 @@ export class NVMeshLoaders {
|
|
|
709
709
|
while (lines[pos].length < 1) {
|
|
710
710
|
pos++
|
|
711
711
|
} // skip blank lines
|
|
712
|
+
if (lines[pos].startsWith('METADATA')) {
|
|
713
|
+
while (lines[pos].length > 1) {
|
|
714
|
+
pos++
|
|
715
|
+
} // skip until blank line
|
|
716
|
+
pos++
|
|
717
|
+
}
|
|
712
718
|
items = lines[pos].trim().split(/\s+/)
|
|
713
719
|
pos++
|
|
714
720
|
if (items[0].includes('LINES')) {
|
|
@@ -909,7 +915,7 @@ export class NVMeshLoaders {
|
|
|
909
915
|
layer.values = obj
|
|
910
916
|
}
|
|
911
917
|
}
|
|
912
|
-
} else if (ext === 'CRV' || ext === 'CURV') {
|
|
918
|
+
} else if (ext === 'CRV' || ext === 'CURV' || ext === 'THICKNESS' || ext === 'AREA') {
|
|
913
919
|
layer.values = NVMeshLoaders.readCURV(buffer, n_vert)
|
|
914
920
|
layer.isTransparentBelowCalMin = false
|
|
915
921
|
} else if (ext === 'GII') {
|
|
@@ -935,6 +941,10 @@ export class NVMeshLoaders {
|
|
|
935
941
|
layer.values = NVMeshLoaders.readSMP(buffer, n_vert)
|
|
936
942
|
} else if (ext === 'STC') {
|
|
937
943
|
layer.values = NVMeshLoaders.readSTC(buffer, n_vert)
|
|
944
|
+
} else if (NVMeshLoaders.isCurv(buffer)) {
|
|
945
|
+
// Unknown layer overlay format - hail mary assume FreeSurfer
|
|
946
|
+
layer.values = NVMeshLoaders.readCURV(buffer, n_vert)
|
|
947
|
+
layer.isTransparentBelowCalMin = false
|
|
938
948
|
} else {
|
|
939
949
|
log.warn('Unknown layer overlay format ' + name)
|
|
940
950
|
return layer
|
|
@@ -1123,6 +1133,19 @@ export class NVMeshLoaders {
|
|
|
1123
1133
|
return f32
|
|
1124
1134
|
} // readSTC()
|
|
1125
1135
|
|
|
1136
|
+
static isCurv(buffer: ArrayBuffer): boolean {
|
|
1137
|
+
const view = new DataView(buffer) // ArrayBuffer to dataview
|
|
1138
|
+
// ALWAYS big endian
|
|
1139
|
+
const sig0 = view.getUint8(0)
|
|
1140
|
+
const sig1 = view.getUint8(1)
|
|
1141
|
+
const sig2 = view.getUint8(2)
|
|
1142
|
+
if (sig0 !== 255 || sig1 !== 255 || sig2 !== 255) {
|
|
1143
|
+
utiltiesLogger.debug('Unable to recognize file type: does not appear to be FreeSurfer format.')
|
|
1144
|
+
return false
|
|
1145
|
+
}
|
|
1146
|
+
return true
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1126
1149
|
// read freesurfer curv big-endian format
|
|
1127
1150
|
// https://github.com/bonilhamusclab/MRIcroS/blob/master/%2BfileUtils/%2Bpial/readPial.m
|
|
1128
1151
|
// http://www.grahamwideman.com/gw/brain/fs/surfacefileformats.htm
|
|
@@ -1171,7 +1194,8 @@ export class NVMeshLoaders {
|
|
|
1171
1194
|
const view = new DataView(buffer) // ArrayBuffer to dataview
|
|
1172
1195
|
// ALWAYS big endian
|
|
1173
1196
|
const n_vertex = view.getUint32(0, false)
|
|
1174
|
-
|
|
1197
|
+
const n_vertexDecimated = this.decimateLayerVertices(n_vertex, n_vert)
|
|
1198
|
+
if (n_vert !== n_vertexDecimated) {
|
|
1175
1199
|
throw new Error('ANNOT file has different number of vertices than mesh')
|
|
1176
1200
|
}
|
|
1177
1201
|
if (buffer.byteLength < 4 + 8 * n_vertex) {
|
|
@@ -2505,6 +2529,23 @@ export class NVMeshLoaders {
|
|
|
2505
2529
|
}
|
|
2506
2530
|
} // readSTL()
|
|
2507
2531
|
|
|
2532
|
+
static decimateLayerVertices(nVertLayer: number, nVertMesh: number): number {
|
|
2533
|
+
// downsample layer vertices if the mesh has been decimated
|
|
2534
|
+
if (nVertLayer % nVertMesh === 0) {
|
|
2535
|
+
return nVertLayer
|
|
2536
|
+
}
|
|
2537
|
+
const V0 = 12
|
|
2538
|
+
const orderLayer = Math.round(Math.log((nVertLayer - 2) / (V0 - 2)) / Math.log(4))
|
|
2539
|
+
const orderMesh = Math.round(Math.log((nVertMesh - 2) / (V0 - 2)) / Math.log(4))
|
|
2540
|
+
// sanity check
|
|
2541
|
+
const nVLayer = Math.pow(4, orderLayer) * (V0 - 2) + 2
|
|
2542
|
+
const nVMesh = Math.pow(4, orderMesh) * (V0 - 2) + 2
|
|
2543
|
+
if (nVLayer !== nVertLayer || nVMesh !== nVertMesh) {
|
|
2544
|
+
return nVertLayer
|
|
2545
|
+
}
|
|
2546
|
+
return nVertMesh
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2508
2549
|
// read NIfTI2 format with embedded CIfTI
|
|
2509
2550
|
// this variation very specific to connectome workbench
|
|
2510
2551
|
// https://brainder.org/2015/04/03/the-nifti-2-file-format/
|
|
@@ -2688,8 +2729,9 @@ export class NVMeshLoaders {
|
|
|
2688
2729
|
//
|
|
2689
2730
|
return scalars
|
|
2690
2731
|
} // is CIfTI
|
|
2732
|
+
nvert = this.decimateLayerVertices(nvert, n_vert)
|
|
2691
2733
|
if (nvert % n_vert !== 0) {
|
|
2692
|
-
throw new Error('Vertices in
|
|
2734
|
+
throw new Error('Vertices in layer (' + nvert + ') is not a multiple of number of vertices (' + n_vert + ')')
|
|
2693
2735
|
}
|
|
2694
2736
|
if (isLittleEndian) {
|
|
2695
2737
|
// block read native endian
|
|
@@ -2776,8 +2818,9 @@ export class NVMeshLoaders {
|
|
|
2776
2818
|
const dim = reader.getUint16(40 + i * 2, isLittleEndian)
|
|
2777
2819
|
nvert *= Math.max(dim, 1)
|
|
2778
2820
|
}
|
|
2821
|
+
nvert = this.decimateLayerVertices(nvert, n_vert)
|
|
2779
2822
|
if (nvert % n_vert !== 0) {
|
|
2780
|
-
throw new Error('Vertices in
|
|
2823
|
+
throw new Error('Vertices in layer (' + nvert + ') is not a multiple of number of vertices (' + n_vert + ')')
|
|
2781
2824
|
}
|
|
2782
2825
|
if (isLittleEndian) {
|
|
2783
2826
|
// block read native endian
|
|
@@ -2835,10 +2878,11 @@ export class NVMeshLoaders {
|
|
|
2835
2878
|
if (version !== 1 || mtype < 0 || mtype > 4) {
|
|
2836
2879
|
log.warn('Not a valid MGH file')
|
|
2837
2880
|
}
|
|
2838
|
-
|
|
2881
|
+
let nvert = width * height * depth * nframes
|
|
2839
2882
|
let scalars: AnyNumberArray = []
|
|
2883
|
+
nvert = this.decimateLayerVertices(nvert, n_vert)
|
|
2840
2884
|
if (nvert % n_vert !== 0) {
|
|
2841
|
-
log.warn('Vertices in
|
|
2885
|
+
log.warn('Vertices in layer (' + nvert + ') is not a multiple of number of vertices (' + n_vert + ')')
|
|
2842
2886
|
return scalars
|
|
2843
2887
|
}
|
|
2844
2888
|
if (mtype === 3) {
|
package/src/nvmesh-utilities.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { vec3 } from 'gl-matrix'
|
|
2
|
+
import { gzipSync } from 'fflate/browser'
|
|
2
3
|
|
|
3
4
|
type Extents = {
|
|
4
5
|
mxDx: number
|
|
@@ -38,6 +39,144 @@ export class NVMeshUtilities {
|
|
|
38
39
|
return border
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
static createMZ3(vertices: Float32Array, indices: Uint32Array, compress: boolean = false): ArrayBuffer {
|
|
43
|
+
// generate binary MZ3 format mesh
|
|
44
|
+
// n.b. small, precise and small but support is not widespread
|
|
45
|
+
// n.b. result can be compressed with gzip
|
|
46
|
+
// https://github.com/neurolabusc/surf-ice/tree/master/mz3
|
|
47
|
+
const magic = 23117
|
|
48
|
+
const attr = 3
|
|
49
|
+
const nface = indices.length / 3
|
|
50
|
+
const nvert = vertices.length / 3
|
|
51
|
+
const nskip = 0
|
|
52
|
+
// Calculate buffer size
|
|
53
|
+
const headerSize = 16
|
|
54
|
+
const indexSize = nface * 3 * 4 // Uint32Array
|
|
55
|
+
const vertexSize = nvert * 3 * 4 // Float32Array
|
|
56
|
+
const totalSize = headerSize + indexSize + vertexSize
|
|
57
|
+
const buffer = new ArrayBuffer(totalSize)
|
|
58
|
+
const writer = new DataView(buffer)
|
|
59
|
+
// Write header
|
|
60
|
+
writer.setUint16(0, magic, true)
|
|
61
|
+
writer.setUint16(2, attr, true)
|
|
62
|
+
writer.setUint32(4, nface, true)
|
|
63
|
+
writer.setUint32(8, nvert, true)
|
|
64
|
+
writer.setUint32(12, nskip, true)
|
|
65
|
+
// Write indices
|
|
66
|
+
let offset = headerSize
|
|
67
|
+
new Uint32Array(buffer, offset, indices.length).set(indices)
|
|
68
|
+
offset += indexSize
|
|
69
|
+
// Write vertices
|
|
70
|
+
new Float32Array(buffer, offset, vertices.length).set(vertices)
|
|
71
|
+
if (compress) {
|
|
72
|
+
return gzipSync(new Uint8Array(buffer))
|
|
73
|
+
}
|
|
74
|
+
return buffer
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static createOBJ(vertices: Float32Array, indices: Uint32Array): ArrayBuffer {
|
|
78
|
+
// generate binary OBJ format mesh
|
|
79
|
+
// n.b. widespread support, but large and slow due to ASCII
|
|
80
|
+
// https://en.wikipedia.org/wiki/Wavefront_.obj_file
|
|
81
|
+
let objContent = ''
|
|
82
|
+
// Add vertices to OBJ content
|
|
83
|
+
for (let i = 0; i < vertices.length; i += 3) {
|
|
84
|
+
objContent += `v ${vertices[i]} ${vertices[i + 1]} ${vertices[i + 2]}\n`
|
|
85
|
+
}
|
|
86
|
+
// Add faces to OBJ content (OBJ indices start at 1, not 0)
|
|
87
|
+
for (let i = 0; i < indices.length; i += 3) {
|
|
88
|
+
objContent += `f ${indices[i] + 1} ${indices[i + 1] + 1} ${indices[i + 2] + 1}\n`
|
|
89
|
+
}
|
|
90
|
+
// Encode the OBJ content as an ArrayBuffer
|
|
91
|
+
const encoder = new TextEncoder()
|
|
92
|
+
const arrayBuffer = encoder.encode(objContent).buffer
|
|
93
|
+
return arrayBuffer
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static createSTL(vertices: Float32Array, indices: Uint32Array): ArrayBuffer {
|
|
97
|
+
// generate binary STL format mesh
|
|
98
|
+
// n.b. inefficient and slow as vertices are not reused
|
|
99
|
+
// https://en.wikipedia.org/wiki/STL_(file_format)#Binary
|
|
100
|
+
const numTriangles = indices.length / 3
|
|
101
|
+
const bufferLength = 84 + numTriangles * 50
|
|
102
|
+
const arrayBuffer = new ArrayBuffer(bufferLength)
|
|
103
|
+
const dataView = new DataView(arrayBuffer)
|
|
104
|
+
// Write header (80 bytes)
|
|
105
|
+
for (let i = 0; i < 80; i++) {
|
|
106
|
+
dataView.setUint8(i, 0)
|
|
107
|
+
}
|
|
108
|
+
// Write number of triangles (4 bytes)
|
|
109
|
+
dataView.setUint32(80, numTriangles, true)
|
|
110
|
+
let offset = 84
|
|
111
|
+
for (let i = 0; i < indices.length; i += 3) {
|
|
112
|
+
const i0 = indices[i] * 3
|
|
113
|
+
const i1 = indices[i + 1] * 3
|
|
114
|
+
const i2 = indices[i + 2] * 3
|
|
115
|
+
// Normal vector (12 bytes, set to zero)
|
|
116
|
+
dataView.setFloat32(offset, 0, true) // Normal X
|
|
117
|
+
dataView.setFloat32(offset + 4, 0, true) // Normal Y
|
|
118
|
+
dataView.setFloat32(offset + 8, 0, true) // Normal Z
|
|
119
|
+
offset += 12
|
|
120
|
+
// Vertex 1 (12 bytes)
|
|
121
|
+
dataView.setFloat32(offset, vertices[i0], true) // Vertex 1 X
|
|
122
|
+
dataView.setFloat32(offset + 4, vertices[i0 + 1], true) // Vertex 1 Y
|
|
123
|
+
dataView.setFloat32(offset + 8, vertices[i0 + 2], true) // Vertex 1 Z
|
|
124
|
+
offset += 12
|
|
125
|
+
// Vertex 2 (12 bytes)
|
|
126
|
+
dataView.setFloat32(offset, vertices[i1], true) // Vertex 2 X
|
|
127
|
+
dataView.setFloat32(offset + 4, vertices[i1 + 1], true) // Vertex 2 Y
|
|
128
|
+
dataView.setFloat32(offset + 8, vertices[i1 + 2], true) // Vertex 2 Z
|
|
129
|
+
offset += 12
|
|
130
|
+
// Vertex 3 (12 bytes)
|
|
131
|
+
dataView.setFloat32(offset, vertices[i2], true) // Vertex 3 X
|
|
132
|
+
dataView.setFloat32(offset + 4, vertices[i2 + 1], true) // Vertex 3 Y
|
|
133
|
+
dataView.setFloat32(offset + 8, vertices[i2 + 2], true) // Vertex 3 Z
|
|
134
|
+
offset += 12
|
|
135
|
+
// Attribute byte count (2 bytes, set to zero)
|
|
136
|
+
dataView.setUint16(offset, 0, true)
|
|
137
|
+
offset += 2
|
|
138
|
+
}
|
|
139
|
+
return arrayBuffer
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
static downloadArrayBuffer(buffer: ArrayBuffer, filename: string): void {
|
|
143
|
+
const blob = new Blob([buffer], { type: 'application/octet-stream' })
|
|
144
|
+
const url = URL.createObjectURL(blob)
|
|
145
|
+
const a = document.createElement('a')
|
|
146
|
+
a.href = url
|
|
147
|
+
a.download = filename
|
|
148
|
+
document.body.appendChild(a)
|
|
149
|
+
a.style.display = 'none'
|
|
150
|
+
a.click()
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
document.body.removeChild(a)
|
|
153
|
+
URL.revokeObjectURL(url)
|
|
154
|
+
}, 0)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
static saveMesh(
|
|
158
|
+
vertices: Float32Array,
|
|
159
|
+
indices: Uint32Array,
|
|
160
|
+
filename: string = '.mz3',
|
|
161
|
+
compress: boolean = false
|
|
162
|
+
): ArrayBuffer {
|
|
163
|
+
let buff = new ArrayBuffer(0)
|
|
164
|
+
if (/\.obj$/i.test(filename)) {
|
|
165
|
+
buff = this.createOBJ(vertices, indices)
|
|
166
|
+
} else if (/\.stl$/i.test(filename)) {
|
|
167
|
+
buff = this.createSTL(vertices, indices)
|
|
168
|
+
} else {
|
|
169
|
+
if (!/\.mz3$/i.test(filename)) {
|
|
170
|
+
filename += '.mz3'
|
|
171
|
+
}
|
|
172
|
+
buff = this.createMZ3(vertices, indices, compress)
|
|
173
|
+
}
|
|
174
|
+
if (filename.length > 4) {
|
|
175
|
+
this.downloadArrayBuffer(buff, filename)
|
|
176
|
+
}
|
|
177
|
+
return buff
|
|
178
|
+
}
|
|
179
|
+
|
|
41
180
|
static getClusterBoundary(rgba8: Uint8Array, faces: number[] | Uint32Array): boolean[] {
|
|
42
181
|
const rgba32 = new Uint32Array(rgba8.buffer)
|
|
43
182
|
// assume all vertices are not near a border
|