@niivue/niivue 0.44.1 → 0.44.2-dev.2
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 +31 -6
- package/build/index.js +531 -135
- package/build/index.js.map +1 -1
- package/build/index.min.js +1 -1
- package/dist/index.d.ts +106 -18
- package/dist/index.js +26377 -30692
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/package.json +18 -17
- package/src/index.html +46 -7
- package/src/index_segment.html +233 -0
- package/src/index_sync.html +333 -0
- package/src/niivue/index.ts +1054 -147
- package/src/niivue.css +0 -1
- package/src/nvdocument.ts +40 -5
- package/src/nvimage/index.ts +4 -1
- package/src/nvimage/utils.ts +61 -42
- package/src/nvlabel.ts +34 -1
- package/src/nvmesh-loaders.ts +24 -1
- package/src/shader-srcs.ts +84 -3
- package/src/types.ts +13 -0
- package/dist/niivue.umd.js +0 -1792
package/src/niivue.css
CHANGED
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,20 @@ 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
|
+
clickToSegmentIs2D: boolean
|
|
154
|
+
// selection box outline thickness
|
|
155
|
+
selectionBoxLineThickness: number
|
|
156
|
+
selectionBoxIsOutline: boolean
|
|
157
|
+
scrollRequiresFocus: boolean
|
|
146
158
|
}
|
|
147
159
|
|
|
148
160
|
export const DEFAULT_OPTIONS: NVConfigOptions = {
|
|
@@ -177,6 +189,7 @@ export const DEFAULT_OPTIONS: NVConfigOptions = {
|
|
|
177
189
|
multiplanarPadPixels: 0,
|
|
178
190
|
// @deprecated
|
|
179
191
|
multiplanarForceRender: false,
|
|
192
|
+
multiplanarEqualSize: false,
|
|
180
193
|
multiplanarShowRender: SHOW_RENDER.AUTO, // auto is the same behaviour as multiplanarForceRender: false
|
|
181
194
|
isRadiologicalConvention: false,
|
|
182
195
|
meshThicknessOn2D: Infinity,
|
|
@@ -210,13 +223,27 @@ export const DEFAULT_OPTIONS: NVConfigOptions = {
|
|
|
210
223
|
renderOverlayBlend: 1.0,
|
|
211
224
|
sliceMosaicString: '',
|
|
212
225
|
centerMosaic: false,
|
|
226
|
+
penSize: 1, // in voxels, since all drawing is done using bitmap indices
|
|
213
227
|
clickToSegment: false,
|
|
214
|
-
clickToSegmentRadius:
|
|
215
|
-
|
|
216
|
-
|
|
228
|
+
clickToSegmentRadius: 3, // in mm
|
|
229
|
+
clickToSegmentBright: true,
|
|
230
|
+
clickToSegmentAutoIntensity: false, // new option, but keep clickToSegmentBright for backwards compatibility
|
|
231
|
+
clickToSegmentIntensityMax: NaN, // NaN will use auto threshold (default flood fill behavior from before)
|
|
232
|
+
clickToSegmentIntensityMin: NaN, // NaN will use auto threshold (default flood fill behavior from before)
|
|
233
|
+
// 0 will use auto threshold (default flood fill behavior from before)
|
|
234
|
+
// Take the voxel intensity at the click point and use this percentage +/- to threshold the flood fill operation.
|
|
235
|
+
// If greater than 0, clickedVoxelIntensity +/- clickedVoxelIntensity * clickToSegmentPercent will be used
|
|
236
|
+
// for the clickToSegmentIntensityMin and clickToSegmentIntensityMax values.
|
|
237
|
+
clickToSegmentPercent: 0,
|
|
238
|
+
clickToSegmentMaxDistanceMM: Number.POSITIVE_INFINITY, // default value is infinity for backwards compatibility with flood fill routine.
|
|
239
|
+
clickToSegmentIs2D: false,
|
|
240
|
+
selectionBoxLineThickness: 4,
|
|
241
|
+
selectionBoxIsOutline: false,
|
|
242
|
+
scrollRequiresFocus: false // determines if the cavas need to be focused to scroll
|
|
217
243
|
}
|
|
218
244
|
|
|
219
245
|
type SceneData = {
|
|
246
|
+
gamma: number
|
|
220
247
|
azimuth: number
|
|
221
248
|
elevation: number
|
|
222
249
|
crosshairPos: vec3
|
|
@@ -230,6 +257,7 @@ type SceneData = {
|
|
|
230
257
|
}
|
|
231
258
|
|
|
232
259
|
export const INITIAL_SCENE_DATA = {
|
|
260
|
+
gamma: 1.0,
|
|
233
261
|
azimuth: 110,
|
|
234
262
|
elevation: 10,
|
|
235
263
|
crosshairPos: vec3.create(),
|
|
@@ -255,6 +283,7 @@ export type Scene = {
|
|
|
255
283
|
pan2Dxyzmm: vec4
|
|
256
284
|
_elevation?: number
|
|
257
285
|
_azimuth?: number
|
|
286
|
+
gamma?: number
|
|
258
287
|
}
|
|
259
288
|
|
|
260
289
|
export type DocumentData = {
|
|
@@ -566,6 +595,12 @@ export class NVDocument {
|
|
|
566
595
|
}
|
|
567
596
|
|
|
568
597
|
data.labels = [...this.data.labels]
|
|
598
|
+
|
|
599
|
+
// remove any handlers
|
|
600
|
+
for (const label of data.labels) {
|
|
601
|
+
delete label.onClick
|
|
602
|
+
}
|
|
603
|
+
|
|
569
604
|
data.customData = this.customData
|
|
570
605
|
|
|
571
606
|
// 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) {
|
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
|
package/src/shader-srcs.ts
CHANGED
|
@@ -719,6 +719,47 @@ void main() {
|
|
|
719
719
|
color = lineColor;
|
|
720
720
|
}`
|
|
721
721
|
|
|
722
|
+
export const fragRectOutlineShader = `#version 300 es
|
|
723
|
+
#line 723
|
|
724
|
+
precision highp int;
|
|
725
|
+
precision highp float;
|
|
726
|
+
|
|
727
|
+
uniform vec4 lineColor;
|
|
728
|
+
uniform vec4 leftTopWidthHeight;
|
|
729
|
+
uniform float thickness; // line thickness in pixels
|
|
730
|
+
uniform vec2 canvasWidthHeight;
|
|
731
|
+
|
|
732
|
+
out vec4 color;
|
|
733
|
+
|
|
734
|
+
void main() {
|
|
735
|
+
// fragment position in screen coordinates
|
|
736
|
+
vec2 fragCoord = gl_FragCoord.xy;
|
|
737
|
+
|
|
738
|
+
// canvas height
|
|
739
|
+
float canvasHeight = canvasWidthHeight.y;
|
|
740
|
+
|
|
741
|
+
// 'top' and 'bottom' to match gl_FragCoord.y coordinate system
|
|
742
|
+
float top = canvasHeight - leftTopWidthHeight.y;
|
|
743
|
+
float bottom = top - leftTopWidthHeight.w;
|
|
744
|
+
|
|
745
|
+
// left and right edges
|
|
746
|
+
float left = leftTopWidthHeight.x;
|
|
747
|
+
float right = left + leftTopWidthHeight.z;
|
|
748
|
+
|
|
749
|
+
bool withinLeft = fragCoord.x >= left && fragCoord.x <= left + thickness;
|
|
750
|
+
bool withinRight = fragCoord.x <= right && fragCoord.x >= right - thickness;
|
|
751
|
+
bool withinTop = fragCoord.y <= top && fragCoord.y >= top - thickness;
|
|
752
|
+
bool withinBottom = fragCoord.y >= bottom && fragCoord.y <= bottom + thickness;
|
|
753
|
+
|
|
754
|
+
bool isOutline = withinLeft || withinRight || withinTop || withinBottom;
|
|
755
|
+
|
|
756
|
+
if (isOutline) {
|
|
757
|
+
color = lineColor;
|
|
758
|
+
} else {
|
|
759
|
+
discard;
|
|
760
|
+
}
|
|
761
|
+
}`
|
|
762
|
+
|
|
722
763
|
export const vertColorbarShader = `#version 300 es
|
|
723
764
|
#line 490
|
|
724
765
|
layout(location=0) in vec3 pos;
|
|
@@ -1207,11 +1248,11 @@ uniform mat4 mvpMtx;
|
|
|
1207
1248
|
uniform mat4 normMtx;
|
|
1208
1249
|
out vec4 vClr;
|
|
1209
1250
|
out vec3 vN;
|
|
1210
|
-
out vec4
|
|
1251
|
+
out vec4 vPc;
|
|
1211
1252
|
void main(void) {
|
|
1212
1253
|
vec3 lightPosition = vec3(0.0, 0.0, -10.0);
|
|
1213
|
-
|
|
1214
|
-
gl_Position =
|
|
1254
|
+
vPc = mvpMtx * vec4(pos, 1.0);
|
|
1255
|
+
gl_Position = vPc;
|
|
1215
1256
|
vN = normalize((normMtx * vec4(norm.xyz,1.0)).xyz);
|
|
1216
1257
|
//vV = -vec3(modelMtx*vec4(pos,1.0));
|
|
1217
1258
|
vClr = clr;
|
|
@@ -1368,6 +1409,46 @@ void main() {
|
|
|
1368
1409
|
color = vec4(d3 + s3 + s, opacity);
|
|
1369
1410
|
}`
|
|
1370
1411
|
|
|
1412
|
+
// https://thorium.rocks/media/curves/curvaceous.html
|
|
1413
|
+
export const fragMeshShaderCrevice = `#version 300 es
|
|
1414
|
+
precision highp int;
|
|
1415
|
+
precision highp float;
|
|
1416
|
+
uniform float opacity;
|
|
1417
|
+
in vec4 vClr;
|
|
1418
|
+
in vec3 vN;
|
|
1419
|
+
in vec4 vPc;
|
|
1420
|
+
out vec4 color;
|
|
1421
|
+
void main() {
|
|
1422
|
+
vec3 n = normalize(vN);
|
|
1423
|
+
// Compute curvature
|
|
1424
|
+
vec3 dx = dFdx(n);
|
|
1425
|
+
vec3 dy = dFdy(n);
|
|
1426
|
+
vec3 xneg = n - dx;
|
|
1427
|
+
vec3 xpos = n + dx;
|
|
1428
|
+
vec3 yneg = n - dy;
|
|
1429
|
+
vec3 ypos = n + dy;
|
|
1430
|
+
float depth = length(vPc.xyz);
|
|
1431
|
+
float curv = (cross(xneg, xpos).y - cross(yneg, ypos).x) / depth;
|
|
1432
|
+
//at this stage 0.5 for flat, with valleys dark and ridges bright
|
|
1433
|
+
curv = 1.0 - (curv + 0.5);
|
|
1434
|
+
//clamp
|
|
1435
|
+
curv = min(max(curv, 0.0), 1.0);
|
|
1436
|
+
// easing function
|
|
1437
|
+
curv = pow(curv, 0.5);
|
|
1438
|
+
//modulate ambient and diffuse with curvature
|
|
1439
|
+
vec3 r = vec3(0.0, 0.0, 1.0); //rayDir: for orthographic projections moving in Z direction (no need for normal matrix)
|
|
1440
|
+
float ambient = 0.6;
|
|
1441
|
+
float diffuse = 0.6;
|
|
1442
|
+
float specular = 0.2;
|
|
1443
|
+
float shininess = 10.0;
|
|
1444
|
+
vec3 lightPosition = vec3(0.0, 10.0, -2.0);
|
|
1445
|
+
vec3 l = normalize(lightPosition);
|
|
1446
|
+
float lightNormDot = dot(n, l);
|
|
1447
|
+
vec3 a = vClr.rgb * ambient * curv;
|
|
1448
|
+
vec3 d = max(lightNormDot, 0.0) * vClr.rgb * diffuse;
|
|
1449
|
+
float s = specular * pow(max(dot(reflect(l, n), r), 0.0), shininess);
|
|
1450
|
+
color = vec4(a + d + s, opacity);
|
|
1451
|
+
}`
|
|
1371
1452
|
// Phong: default
|
|
1372
1453
|
export const fragMeshShader = `#version 300 es
|
|
1373
1454
|
precision highp int;
|
package/src/types.ts
CHANGED
|
@@ -152,3 +152,16 @@ export type NiiVueLocation = {
|
|
|
152
152
|
vox: vec3
|
|
153
153
|
xy: [number, number]
|
|
154
154
|
}
|
|
155
|
+
|
|
156
|
+
export type SyncOpts = {
|
|
157
|
+
'3d'?: boolean
|
|
158
|
+
'2d'?: boolean
|
|
159
|
+
zoomPan?: boolean
|
|
160
|
+
cal_min?: boolean
|
|
161
|
+
cal_max?: boolean
|
|
162
|
+
gamma?: boolean
|
|
163
|
+
useSliceOffset?: boolean
|
|
164
|
+
sliceType?: boolean
|
|
165
|
+
crosshair?: boolean
|
|
166
|
+
clipPlane?: boolean
|
|
167
|
+
}
|