@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/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
vertLineShader,
|
|
13
13
|
vertLine3DShader,
|
|
14
14
|
fragRectShader,
|
|
15
|
+
fragRectOutlineShader,
|
|
15
16
|
vertRenderShader,
|
|
16
17
|
fragRenderShader,
|
|
17
18
|
fragRenderGradientShader,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
fragMeshMatcapShader,
|
|
42
43
|
fragMeshOutlineShader,
|
|
43
44
|
fragMeshEdgeShader,
|
|
45
|
+
fragMeshShaderCrevice,
|
|
44
46
|
fragMeshDiffuseEdgeShader,
|
|
45
47
|
fragMeshHemiShader,
|
|
46
48
|
fragMeshMatteShader,
|
|
@@ -78,7 +80,14 @@ import {
|
|
|
78
80
|
INITIAL_SCENE_DATA
|
|
79
81
|
} from '../nvdocument.js'
|
|
80
82
|
|
|
81
|
-
import {
|
|
83
|
+
import {
|
|
84
|
+
LabelTextAlignment,
|
|
85
|
+
LabelLineTerminator,
|
|
86
|
+
NVLabel3D,
|
|
87
|
+
NVLabel3DStyle,
|
|
88
|
+
LabelAnchorPoint,
|
|
89
|
+
LabelAnchorFlag
|
|
90
|
+
} from '../nvlabel.js'
|
|
82
91
|
import { FreeSurferConnectome, NVConnectome } from '../nvconnectome.js'
|
|
83
92
|
import {
|
|
84
93
|
NVImage,
|
|
@@ -96,7 +105,8 @@ import {
|
|
|
96
105
|
NiftiHeader,
|
|
97
106
|
DragReleaseParams,
|
|
98
107
|
NiiVueLocation,
|
|
99
|
-
NiiVueLocationValue
|
|
108
|
+
NiiVueLocationValue,
|
|
109
|
+
SyncOpts
|
|
100
110
|
} from '../types.js'
|
|
101
111
|
import {
|
|
102
112
|
clamp,
|
|
@@ -121,7 +131,7 @@ export { NVImage, NVImageFromUrlOptions } from '../nvimage/index.js'
|
|
|
121
131
|
// address rollup error - https://github.com/rollup/plugins/issues/71
|
|
122
132
|
export * from '../nvdocument.js'
|
|
123
133
|
export { NVUtilities } from '../nvutilities.js'
|
|
124
|
-
export { LabelTextAlignment, LabelLineTerminator, NVLabel3DStyle, NVLabel3D } from '../nvlabel.js'
|
|
134
|
+
export { LabelTextAlignment, LabelLineTerminator, NVLabel3DStyle, NVLabel3D, LabelAnchorPoint } from '../nvlabel.js'
|
|
125
135
|
export { NVMeshLoaders } from '../nvmesh-loaders.js'
|
|
126
136
|
export { NVMeshUtilities } from '../nvmesh-utilities.js'
|
|
127
137
|
|
|
@@ -186,6 +196,7 @@ type Descriptive = {
|
|
|
186
196
|
cal_max: number
|
|
187
197
|
robust_min: number
|
|
188
198
|
robust_max: number
|
|
199
|
+
area: number | null
|
|
189
200
|
}
|
|
190
201
|
|
|
191
202
|
type SliceScale = {
|
|
@@ -334,6 +345,9 @@ export class Niivue {
|
|
|
334
345
|
drawUndoBitmaps: Uint8Array[] = [] // array of drawBitmaps for undo
|
|
335
346
|
drawLut = cmapper.makeDrawLut('$itksnap') // the color lookup table for drawing
|
|
336
347
|
drawOpacity = 0.8 // opacity of drawing (default)
|
|
348
|
+
clickToSegmentIsGrowing = false // flag to indicate if the clickToSegment flood fill growing is in progress with left mouse down + drag
|
|
349
|
+
clickToSegmentGrowingBitmap: Uint8Array | null = null // the bitmap of the growing flood fill
|
|
350
|
+
clickToSegmentXY = [0, 0] // the x,y location of the clickToSegment flood fill
|
|
337
351
|
renderDrawAmbientOcclusion = 0.4
|
|
338
352
|
colorbarHeight = 0 // height in pixels, set when colorbar is drawn
|
|
339
353
|
drawPenLocation = [NaN, NaN, NaN]
|
|
@@ -347,6 +361,7 @@ export class Niivue {
|
|
|
347
361
|
orientCubeShader?: Shader
|
|
348
362
|
orientCubeShaderVAO: WebGLVertexArrayObject | null = null
|
|
349
363
|
rectShader?: Shader
|
|
364
|
+
rectOutlineShader?: Shader
|
|
350
365
|
renderShader?: Shader
|
|
351
366
|
lineShader?: Shader
|
|
352
367
|
line3DShader?: Shader
|
|
@@ -392,7 +407,19 @@ export class Niivue {
|
|
|
392
407
|
extentsMax?: vec3
|
|
393
408
|
// ResizeObserver
|
|
394
409
|
private resizeObserver: ResizeObserver | null = null
|
|
395
|
-
syncOpts: Record<string, unknown> = {}
|
|
410
|
+
// syncOpts: Record<string, unknown> = {}
|
|
411
|
+
syncOpts: SyncOpts = {
|
|
412
|
+
'3d': false, // legacy option
|
|
413
|
+
'2d': false, // legacy option
|
|
414
|
+
zoomPan: false,
|
|
415
|
+
cal_min: false,
|
|
416
|
+
cal_max: false,
|
|
417
|
+
clipPlane: false,
|
|
418
|
+
gamma: false,
|
|
419
|
+
sliceType: false,
|
|
420
|
+
crosshair: false
|
|
421
|
+
}
|
|
422
|
+
|
|
396
423
|
readyForSync = false
|
|
397
424
|
|
|
398
425
|
// UI Data
|
|
@@ -442,7 +469,7 @@ export class Niivue {
|
|
|
442
469
|
|
|
443
470
|
cuboidVertexBuffer?: WebGLBuffer
|
|
444
471
|
|
|
445
|
-
otherNV: Niivue
|
|
472
|
+
otherNV: Niivue[] | null = null // another niivue instance that we wish to sync position with
|
|
446
473
|
volumeObject3D: NiivueObject3D | null = null
|
|
447
474
|
pivot3D = [0, 0, 0] // center for rendering rotation
|
|
448
475
|
furthestFromPivot = 10.0 // most distant point from pivot
|
|
@@ -479,6 +506,10 @@ export class Niivue {
|
|
|
479
506
|
Name: 'Hemispheric',
|
|
480
507
|
Frag: fragMeshHemiShader
|
|
481
508
|
},
|
|
509
|
+
{
|
|
510
|
+
Name: 'Crevice',
|
|
511
|
+
Frag: fragMeshShaderCrevice
|
|
512
|
+
},
|
|
482
513
|
{
|
|
483
514
|
Name: 'Edge',
|
|
484
515
|
Frag: fragMeshEdgeShader
|
|
@@ -907,7 +938,11 @@ export class Niivue {
|
|
|
907
938
|
* @deprecated use broadcastTo instead
|
|
908
939
|
* @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
|
|
909
940
|
*/
|
|
910
|
-
syncWith(otherNV: Niivue, syncOpts = { '2d': true, '3d': true }): void {
|
|
941
|
+
syncWith(otherNV: Niivue | Niivue[], syncOpts = { '2d': true, '3d': true }): void {
|
|
942
|
+
// if otherNV is not an array, make it an array of one
|
|
943
|
+
if (!(otherNV instanceof Array)) {
|
|
944
|
+
otherNV = [otherNV]
|
|
945
|
+
}
|
|
911
946
|
this.otherNV = otherNV
|
|
912
947
|
this.syncOpts = { ...syncOpts }
|
|
913
948
|
}
|
|
@@ -924,10 +959,71 @@ export class Niivue {
|
|
|
924
959
|
* @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
|
|
925
960
|
*/
|
|
926
961
|
broadcastTo(otherNV: Niivue | Niivue[], syncOpts = { '2d': true, '3d': true }): void {
|
|
962
|
+
// if otherNV is a single instance then make it an array of one
|
|
963
|
+
if (!(otherNV instanceof Array)) {
|
|
964
|
+
otherNV = [otherNV]
|
|
965
|
+
}
|
|
927
966
|
this.otherNV = otherNV
|
|
928
967
|
this.syncOpts = syncOpts
|
|
929
968
|
}
|
|
930
969
|
|
|
970
|
+
doSync3d(otherNV: Niivue): void {
|
|
971
|
+
otherNV.scene.renderAzimuth = this.scene.renderAzimuth
|
|
972
|
+
otherNV.scene.renderElevation = this.scene.renderElevation
|
|
973
|
+
otherNV.scene.volScaleMultiplier = this.scene.volScaleMultiplier
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// both crosshair and zoomPan
|
|
977
|
+
doSync2d(otherNV: Niivue): void {
|
|
978
|
+
const thisMM = this.frac2mm(this.scene.crosshairPos)
|
|
979
|
+
otherNV.scene.crosshairPos = otherNV.mm2frac(thisMM)
|
|
980
|
+
otherNV.scene.pan2Dxyzmm = vec4.clone(this.scene.pan2Dxyzmm)
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
doSyncGamma(otherNV: Niivue): void {
|
|
984
|
+
// gamma not dependent on 2d/3d
|
|
985
|
+
const thisGamma = this.scene.gamma
|
|
986
|
+
const otherGamma = otherNV.scene.gamma
|
|
987
|
+
if (thisGamma !== otherGamma) {
|
|
988
|
+
otherNV.setGamma(thisGamma)
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
doSyncZoomPan(otherNV: Niivue): void {
|
|
993
|
+
otherNV.scene.pan2Dxyzmm = vec4.clone(this.scene.pan2Dxyzmm)
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
doSyncCrosshair(otherNV: Niivue): void {
|
|
997
|
+
const thisMM = this.frac2mm(this.scene.crosshairPos)
|
|
998
|
+
otherNV.scene.crosshairPos = otherNV.mm2frac(thisMM)
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
doSyncCalMin(otherNV: Niivue): void {
|
|
1002
|
+
// only call updateGLVolume if the cal_min is different
|
|
1003
|
+
// because updateGLVolume is expensive, but required to update the volume
|
|
1004
|
+
if (this.volumes[0].cal_min !== otherNV.volumes[0].cal_min) {
|
|
1005
|
+
otherNV.volumes[0].cal_min = this.volumes[0].cal_min
|
|
1006
|
+
otherNV.updateGLVolume()
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
doSyncCalMax(otherNV: Niivue): void {
|
|
1011
|
+
// only call updateGLVolume if the cal_max is different
|
|
1012
|
+
// because updateGLVolume is expensive, but required to update the volume
|
|
1013
|
+
if (this.volumes[0].cal_max !== otherNV.volumes[0].cal_max) {
|
|
1014
|
+
otherNV.volumes[0].cal_max = this.volumes[0].cal_max
|
|
1015
|
+
otherNV.updateGLVolume()
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
doSyncSliceType(otherNV: Niivue): void {
|
|
1020
|
+
otherNV.setSliceType(this.opts.sliceType)
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
doSyncClipPlane(otherNV: Niivue): void {
|
|
1024
|
+
otherNV.setClipPlane(this.scene.clipPlaneDepthAziElev)
|
|
1025
|
+
}
|
|
1026
|
+
|
|
931
1027
|
/**
|
|
932
1028
|
* Sync the scene controls (orientation, crosshair location, etc.) from one Niivue instance to another. useful for using one canvas to drive another.
|
|
933
1029
|
* @internal
|
|
@@ -941,44 +1037,52 @@ export class Niivue {
|
|
|
941
1037
|
if (!this.gl || !this.otherNV || typeof this.otherNV === 'undefined') {
|
|
942
1038
|
return
|
|
943
1039
|
}
|
|
944
|
-
// if (!this.otherNV.readyForSync || !this.readyForSync) {
|
|
945
|
-
// return;
|
|
946
|
-
// }
|
|
947
1040
|
// canvas must have focus to send messages issue706
|
|
948
1041
|
if (!(this.gl.canvas as HTMLCanvasElement).matches(':focus')) {
|
|
949
1042
|
return
|
|
950
1043
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1044
|
+
for (let i = 0; i < this.otherNV.length; i++) {
|
|
1045
|
+
if (this.otherNV[i] === this) {
|
|
1046
|
+
continue
|
|
1047
|
+
}
|
|
1048
|
+
// gamma
|
|
1049
|
+
if (this.syncOpts.gamma) {
|
|
1050
|
+
this.doSyncGamma(this.otherNV[i])
|
|
1051
|
+
}
|
|
1052
|
+
// crosshair
|
|
1053
|
+
if (this.syncOpts.crosshair) {
|
|
1054
|
+
this.doSyncCrosshair(this.otherNV[i])
|
|
1055
|
+
}
|
|
1056
|
+
// zoomPan
|
|
1057
|
+
if (this.syncOpts.zoomPan) {
|
|
1058
|
+
this.doSyncZoomPan(this.otherNV[i])
|
|
1059
|
+
}
|
|
1060
|
+
// sliceType
|
|
1061
|
+
if (this.syncOpts.sliceType) {
|
|
1062
|
+
this.doSyncSliceType(this.otherNV[i])
|
|
1063
|
+
}
|
|
1064
|
+
// cal_min
|
|
1065
|
+
if (this.syncOpts.cal_min) {
|
|
1066
|
+
this.doSyncCalMin(this.otherNV[i])
|
|
1067
|
+
}
|
|
1068
|
+
// cal_max
|
|
1069
|
+
if (this.syncOpts.cal_max) {
|
|
1070
|
+
this.doSyncCalMax(this.otherNV[i])
|
|
1071
|
+
}
|
|
1072
|
+
// clipPlane
|
|
1073
|
+
if (this.syncOpts.clipPlane) {
|
|
1074
|
+
this.doSyncClipPlane(this.otherNV[i])
|
|
1075
|
+
}
|
|
1076
|
+
// legacy 2d option for multiple properties
|
|
954
1077
|
if (this.syncOpts['2d']) {
|
|
955
|
-
this.
|
|
956
|
-
this.otherNV.scene.pan2Dxyzmm = vec4.clone(this.scene.pan2Dxyzmm)
|
|
1078
|
+
this.doSync2d(this.otherNV[i])
|
|
957
1079
|
}
|
|
1080
|
+
// legacy 3d option for multiple properties
|
|
958
1081
|
if (this.syncOpts['3d']) {
|
|
959
|
-
this.
|
|
960
|
-
this.otherNV.scene.renderElevation = this.scene.renderElevation
|
|
961
|
-
this.otherNV.scene.volScaleMultiplier = this.scene.volScaleMultiplier
|
|
962
|
-
}
|
|
963
|
-
this.otherNV.drawScene()
|
|
964
|
-
this.otherNV.createOnLocationChange()
|
|
965
|
-
} else if (Array.isArray(this.otherNV)) {
|
|
966
|
-
for (let i = 0; i < this.otherNV.length; i++) {
|
|
967
|
-
if (this.otherNV[i] === this) {
|
|
968
|
-
continue
|
|
969
|
-
}
|
|
970
|
-
if (this.syncOpts['2d']) {
|
|
971
|
-
this.otherNV[i].scene.crosshairPos = this.otherNV[i].mm2frac(thisMM)
|
|
972
|
-
this.otherNV[i].scene.pan2Dxyzmm = vec4.clone(this.scene.pan2Dxyzmm)
|
|
973
|
-
}
|
|
974
|
-
if (this.syncOpts['3d']) {
|
|
975
|
-
this.otherNV[i].scene.renderAzimuth = this.scene.renderAzimuth
|
|
976
|
-
this.otherNV[i].scene.renderElevation = this.scene.renderElevation
|
|
977
|
-
this.otherNV[i].scene.volScaleMultiplier = this.scene.volScaleMultiplier
|
|
978
|
-
}
|
|
979
|
-
this.otherNV[i].drawScene()
|
|
980
|
-
this.otherNV[i].createOnLocationChange()
|
|
1082
|
+
this.doSync3d(this.otherNV[i])
|
|
981
1083
|
}
|
|
1084
|
+
this.otherNV[i].drawScene()
|
|
1085
|
+
this.otherNV[i].createOnLocationChange()
|
|
982
1086
|
}
|
|
983
1087
|
}
|
|
984
1088
|
|
|
@@ -1088,6 +1192,9 @@ export class Niivue {
|
|
|
1088
1192
|
this.drawPenLocation = [NaN, NaN, NaN]
|
|
1089
1193
|
this.drawPenAxCorSag = -1
|
|
1090
1194
|
this.uiData.mousedown = true
|
|
1195
|
+
// reset drag positions used previously.
|
|
1196
|
+
this.setDragStart(0, 0)
|
|
1197
|
+
this.setDragEnd(0, 0)
|
|
1091
1198
|
log.debug('mouse down')
|
|
1092
1199
|
log.debug(e)
|
|
1093
1200
|
// record tile where mouse clicked
|
|
@@ -1097,8 +1204,16 @@ export class Niivue {
|
|
|
1097
1204
|
}
|
|
1098
1205
|
|
|
1099
1206
|
const [x, y] = [pos.x * this.uiData.dpr!, pos.y * this.uiData.dpr!]
|
|
1207
|
+
if (this.opts.clickToSegment) {
|
|
1208
|
+
this.clickToSegmentXY = [x, y]
|
|
1209
|
+
}
|
|
1100
1210
|
const label = this.getLabelAtPoint([x, y])
|
|
1101
1211
|
if (label) {
|
|
1212
|
+
// check for user defined onclick handler
|
|
1213
|
+
if (label.onClick) {
|
|
1214
|
+
label.onClick(label)
|
|
1215
|
+
return
|
|
1216
|
+
}
|
|
1102
1217
|
// find associated mesh
|
|
1103
1218
|
for (const mesh of this.meshes) {
|
|
1104
1219
|
if (mesh.type !== MeshType.CONNECTOME) {
|
|
@@ -1275,7 +1390,6 @@ export class Niivue {
|
|
|
1275
1390
|
const mmLength = vec3.len(v)
|
|
1276
1391
|
const voxStart = this.frac2vox(fracStart)
|
|
1277
1392
|
const voxEnd = this.frac2vox(fracEnd)
|
|
1278
|
-
|
|
1279
1393
|
this.onDragRelease({
|
|
1280
1394
|
fracStart,
|
|
1281
1395
|
fracEnd,
|
|
@@ -1313,8 +1427,18 @@ export class Niivue {
|
|
|
1313
1427
|
if (this.drawPenFillPts.length > 0) {
|
|
1314
1428
|
this.drawPenFilled()
|
|
1315
1429
|
} else if (this.drawPenAxCorSag >= 0) {
|
|
1316
|
-
this.
|
|
1430
|
+
if (this.opts.clickToSegment) {
|
|
1431
|
+
// copy clickToSegment bitmap to drawBitmap.
|
|
1432
|
+
// determine which bitmap has more non-zero values, indicating it is more recent
|
|
1433
|
+
const sumBitmap = this.sumBitmap(this.drawBitmap)
|
|
1434
|
+
const sumSeg = this.sumBitmap(this.clickToSegmentGrowingBitmap)
|
|
1435
|
+
if (sumSeg > sumBitmap) {
|
|
1436
|
+
this.updateBitmapFromClickToSegment()
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1317
1439
|
}
|
|
1440
|
+
// add the bitmap after the clickToSegment update
|
|
1441
|
+
this.drawAddUndoBitmap()
|
|
1318
1442
|
this.drawPenLocation = [NaN, NaN, NaN]
|
|
1319
1443
|
this.drawPenAxCorSag = -1
|
|
1320
1444
|
if (isFunction(this.onMouseUp)) {
|
|
@@ -1328,6 +1452,11 @@ export class Niivue {
|
|
|
1328
1452
|
const fracStart = this.canvasPos2frac([this.uiData.dragStart[0], this.uiData.dragStart[1]])
|
|
1329
1453
|
const fracEnd = this.canvasPos2frac([this.uiData.dragEnd[0], this.uiData.dragEnd[1]])
|
|
1330
1454
|
this.generateMouseUpCallback(fracStart, fracEnd)
|
|
1455
|
+
// if roiSelection drag mode
|
|
1456
|
+
if (this.opts.dragMode === DRAG_MODE.roiSelection) {
|
|
1457
|
+
// do not call drawScene so that the selection box remains visible
|
|
1458
|
+
return
|
|
1459
|
+
}
|
|
1331
1460
|
if (this.opts.dragMode !== DRAG_MODE.contrast) {
|
|
1332
1461
|
return
|
|
1333
1462
|
}
|
|
@@ -1447,6 +1576,16 @@ export class Niivue {
|
|
|
1447
1576
|
this.drawScene()
|
|
1448
1577
|
this.uiData.prevX = this.uiData.currX
|
|
1449
1578
|
this.uiData.prevY = this.uiData.currY
|
|
1579
|
+
} else if (!this.uiData.mousedown && this.opts.clickToSegment) {
|
|
1580
|
+
const pos = this.getNoPaddingNoBorderCanvasRelativeMousePosition(e, this.gl.canvas)
|
|
1581
|
+
// ignore if mouse moves outside of tile of initial click
|
|
1582
|
+
if (!pos) {
|
|
1583
|
+
return
|
|
1584
|
+
}
|
|
1585
|
+
const x = pos.x * this.uiData.dpr!
|
|
1586
|
+
const y = pos.y * this.uiData.dpr!
|
|
1587
|
+
this.mousePos = [x, y]
|
|
1588
|
+
this.drawScene()
|
|
1450
1589
|
}
|
|
1451
1590
|
}
|
|
1452
1591
|
|
|
@@ -1664,6 +1803,40 @@ export class Niivue {
|
|
|
1664
1803
|
if (this.thumbnailVisible) {
|
|
1665
1804
|
return
|
|
1666
1805
|
}
|
|
1806
|
+
// check that the user has actually created an ROI already.
|
|
1807
|
+
const dragStartSum = this.uiData.dragStart.reduce((a, b) => a + b, 0)
|
|
1808
|
+
const dragEndSum = this.uiData.dragEnd.reduce((a, b) => a + b, 0)
|
|
1809
|
+
const validDrag = dragStartSum > 0 && dragEndSum > 0
|
|
1810
|
+
// if dragMode is roiSelection, grow or shrink the selection box
|
|
1811
|
+
// by scrolling the mouse wheel. Grows by 1 pixel per scroll
|
|
1812
|
+
if (this.opts.dragMode === DRAG_MODE.roiSelection && validDrag) {
|
|
1813
|
+
const delta = e.deltaY > 0 ? 1 : -1
|
|
1814
|
+
// update the uiData.dragStart and uiData.dragEnd values to grow or shrink the selection box
|
|
1815
|
+
if (this.uiData.dragStart[0] < this.uiData.dragEnd[0]) {
|
|
1816
|
+
this.uiData.dragStart[0] -= delta
|
|
1817
|
+
this.uiData.dragEnd[0] += delta
|
|
1818
|
+
} else {
|
|
1819
|
+
this.uiData.dragStart[0] += delta
|
|
1820
|
+
this.uiData.dragEnd[0] -= delta
|
|
1821
|
+
}
|
|
1822
|
+
if (this.uiData.dragStart[1] < this.uiData.dragEnd[1]) {
|
|
1823
|
+
this.uiData.dragStart[1] -= delta
|
|
1824
|
+
this.uiData.dragEnd[1] += delta
|
|
1825
|
+
} else {
|
|
1826
|
+
this.uiData.dragStart[1] += delta
|
|
1827
|
+
this.uiData.dragEnd[1] -= delta
|
|
1828
|
+
}
|
|
1829
|
+
// draw the scene
|
|
1830
|
+
this.uiData.isDragging = true // set is dragging so the selection box is drawn for this drawScene call
|
|
1831
|
+
this.drawScene() // drawScene uses isDragging to determine if the selection box or ROI should be drawn
|
|
1832
|
+
this.uiData.isDragging = false // reset is dragging
|
|
1833
|
+
const tileIdx = this.tileIndex(this.uiData.dragStart[0], this.uiData.dragStart[1])
|
|
1834
|
+
this.generateMouseUpCallback(
|
|
1835
|
+
this.screenXY2TextureFrac(this.uiData.dragStart[0], this.uiData.dragStart[1], tileIdx),
|
|
1836
|
+
this.screenXY2TextureFrac(this.uiData.dragEnd[0], this.uiData.dragEnd[1], tileIdx)
|
|
1837
|
+
)
|
|
1838
|
+
return
|
|
1839
|
+
}
|
|
1667
1840
|
const rect = this.canvas!.getBoundingClientRect()
|
|
1668
1841
|
if (e.deltaY < 0) {
|
|
1669
1842
|
this.sliceScroll2D(-0.01, e.clientX - rect.left, e.clientY - rect.top)
|
|
@@ -2051,7 +2224,7 @@ export class Niivue {
|
|
|
2051
2224
|
this.addVolume(volume)
|
|
2052
2225
|
// set drawing enabled to make sure
|
|
2053
2226
|
// the new drawing bitmap matches the background volume dims
|
|
2054
|
-
this.setDrawingEnabled(true)
|
|
2227
|
+
// this.setDrawingEnabled(true)
|
|
2055
2228
|
}
|
|
2056
2229
|
})
|
|
2057
2230
|
.catch((e) => {
|
|
@@ -3262,6 +3435,12 @@ export class Niivue {
|
|
|
3262
3435
|
|
|
3263
3436
|
// not included in public docs
|
|
3264
3437
|
sliceScroll2D(posChange: number, x: number, y: number, isDelta = true): void {
|
|
3438
|
+
// check if the canvas has focus
|
|
3439
|
+
if (this.opts.scrollRequiresFocus && this.canvas !== document.activeElement) {
|
|
3440
|
+
log.warn('Canvas element does not have focus. Scroll events will not be processed.')
|
|
3441
|
+
return
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3265
3444
|
if (this.inGraphTile(x, y)) {
|
|
3266
3445
|
let vol = this.volumes[0].frame4D
|
|
3267
3446
|
if (posChange > 0) {
|
|
@@ -4005,6 +4184,7 @@ export class Niivue {
|
|
|
4005
4184
|
} // something is horribly wrong!
|
|
4006
4185
|
const vx = this.back.dims[1] * this.back.dims[2] * this.back.dims[3]
|
|
4007
4186
|
this.drawBitmap = new Uint8Array(vx)
|
|
4187
|
+
this.clickToSegmentGrowingBitmap = new Uint8Array(vx)
|
|
4008
4188
|
this.drawClearAllUndoBitmaps()
|
|
4009
4189
|
this.drawAddUndoBitmap()
|
|
4010
4190
|
this.drawTexture = this.r8Tex(this.drawTexture, TEXTURE7_DRAW, this.back.dims, true)
|
|
@@ -4193,6 +4373,29 @@ export class Niivue {
|
|
|
4193
4373
|
y = Math.min(Math.max(y, 0), dy - 1)
|
|
4194
4374
|
z = Math.min(Math.max(z, 0), dz - 1)
|
|
4195
4375
|
this.drawBitmap![x + y * dx + z * dx * dy] = penValue
|
|
4376
|
+
// get tile index for voxel
|
|
4377
|
+
const isAx = this.drawPenAxCorSag === 0
|
|
4378
|
+
const isCor = this.drawPenAxCorSag === 1
|
|
4379
|
+
const isSag = this.drawPenAxCorSag === 2
|
|
4380
|
+
// since the pen is only drawing in one 2D plane,
|
|
4381
|
+
// only draw the neighbors (based on penSize) in that plane.
|
|
4382
|
+
// if penSize is 1, only draw the voxel itself.
|
|
4383
|
+
// if penSize is even (2, 4, 6, etc.), then the extra voxel will be drawn in the positive direction.
|
|
4384
|
+
// if penSize is odd (3, 5, 7, etc.), then the the pen will be centered on the voxel.
|
|
4385
|
+
if (this.opts.penSize > 1) {
|
|
4386
|
+
const halfPenSize = Math.floor(this.opts.penSize / 2)
|
|
4387
|
+
for (let i = -halfPenSize; i <= halfPenSize; i++) {
|
|
4388
|
+
for (let j = -halfPenSize; j <= halfPenSize; j++) {
|
|
4389
|
+
if (isAx) {
|
|
4390
|
+
this.drawBitmap![x + i + (y + j) * dx + z * dx * dy] = penValue
|
|
4391
|
+
} else if (isCor) {
|
|
4392
|
+
this.drawBitmap![x + i + y * dx + (z + j) * dx * dy] = penValue
|
|
4393
|
+
} else if (isSag) {
|
|
4394
|
+
this.drawBitmap![x + (y + j) * dx + (z + i) * dx * dy] = penValue
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4196
4399
|
}
|
|
4197
4400
|
|
|
4198
4401
|
// not included in public docs
|
|
@@ -4279,6 +4482,140 @@ export class Niivue {
|
|
|
4279
4482
|
}
|
|
4280
4483
|
}
|
|
4281
4484
|
|
|
4485
|
+
/**
|
|
4486
|
+
* Performs a 1-voxel binary dilation on a connected cluster within the drawing mask using the drawFloodFillCore function.
|
|
4487
|
+
*
|
|
4488
|
+
* @param seedXYZ - voxel index of the seed voxel in the mask array.
|
|
4489
|
+
* @param neighbors - Number of neighbors to consider for connectivity and dilation (6, 18, or 26).
|
|
4490
|
+
*/
|
|
4491
|
+
drawingBinaryDilationWithSeed(
|
|
4492
|
+
seedXYZ: number[], // seed voxel x,y,z
|
|
4493
|
+
neighbors: 6 | 18 | 26 = 6
|
|
4494
|
+
): void {
|
|
4495
|
+
try {
|
|
4496
|
+
const mask = this.drawBitmap
|
|
4497
|
+
const xDim = this.back.dims[1]
|
|
4498
|
+
const yDim = this.back.dims[2]
|
|
4499
|
+
const zDim = this.back.dims[3]
|
|
4500
|
+
const nx = xDim
|
|
4501
|
+
const nxy = xDim * yDim
|
|
4502
|
+
const totalVoxels = nxy * zDim
|
|
4503
|
+
function xyz2vx(pt: number[]): number {
|
|
4504
|
+
return pt[0] + pt[1] * nx + pt[2] * nxy
|
|
4505
|
+
}
|
|
4506
|
+
|
|
4507
|
+
const seedIndex = xyz2vx(seedXYZ)
|
|
4508
|
+
|
|
4509
|
+
// check that the seed index is within bounds
|
|
4510
|
+
if (seedIndex < 0 || seedIndex >= totalVoxels) {
|
|
4511
|
+
throw new Error('Seed index is out of bounds.')
|
|
4512
|
+
}
|
|
4513
|
+
|
|
4514
|
+
// get value of the seed voxel
|
|
4515
|
+
const seedValue = mask[seedIndex]
|
|
4516
|
+
|
|
4517
|
+
// check that the seed voxel is filled
|
|
4518
|
+
if (seedValue === 0) {
|
|
4519
|
+
throw new Error('Seed voxel is not part of a filled cluster.')
|
|
4520
|
+
}
|
|
4521
|
+
|
|
4522
|
+
// create a copy of the mask to work on
|
|
4523
|
+
const img = mask.slice()
|
|
4524
|
+
// binarise the img since there could be multiple colors in the mask
|
|
4525
|
+
for (let i = 0; i < totalVoxels; i++) {
|
|
4526
|
+
img[i] = img[i] === seedValue ? 1 : 0
|
|
4527
|
+
}
|
|
4528
|
+
|
|
4529
|
+
// use drawFloodFillCore to identify the connected cluster starting from seedIndex
|
|
4530
|
+
this.drawFloodFillCore(img, seedIndex, neighbors)
|
|
4531
|
+
|
|
4532
|
+
// now, img has the cluster marked with value 2
|
|
4533
|
+
// create an output mask for dilation
|
|
4534
|
+
const outputMask = mask.slice() // Clone the original mask
|
|
4535
|
+
|
|
4536
|
+
// precompute neighbor offsets based on connectivity
|
|
4537
|
+
const neighborOffsets: number[] = []
|
|
4538
|
+
|
|
4539
|
+
// offsets for 6-connectivity (face neighbors)
|
|
4540
|
+
const offsets6 = [-nxy, nxy, -xDim, xDim, -1, 1]
|
|
4541
|
+
|
|
4542
|
+
neighborOffsets.push(...offsets6)
|
|
4543
|
+
|
|
4544
|
+
if (neighbors > 6) {
|
|
4545
|
+
// offsets for 18-connectivity (edge neighbors)
|
|
4546
|
+
neighborOffsets.push(
|
|
4547
|
+
-xDim - 1,
|
|
4548
|
+
-xDim + 1,
|
|
4549
|
+
xDim - 1,
|
|
4550
|
+
xDim + 1,
|
|
4551
|
+
-nxy - xDim,
|
|
4552
|
+
-nxy + xDim,
|
|
4553
|
+
-nxy - 1,
|
|
4554
|
+
-nxy + 1,
|
|
4555
|
+
nxy - xDim,
|
|
4556
|
+
nxy + xDim,
|
|
4557
|
+
nxy - 1,
|
|
4558
|
+
nxy + 1
|
|
4559
|
+
)
|
|
4560
|
+
}
|
|
4561
|
+
|
|
4562
|
+
if (neighbors > 18) {
|
|
4563
|
+
// offsets for 26-connectivity (corner neighbors)
|
|
4564
|
+
neighborOffsets.push(
|
|
4565
|
+
-nxy - xDim - 1,
|
|
4566
|
+
-nxy - xDim + 1,
|
|
4567
|
+
-nxy + xDim - 1,
|
|
4568
|
+
-nxy + xDim + 1,
|
|
4569
|
+
nxy - xDim - 1,
|
|
4570
|
+
nxy - xDim + 1,
|
|
4571
|
+
nxy + xDim - 1,
|
|
4572
|
+
nxy + xDim + 1
|
|
4573
|
+
)
|
|
4574
|
+
}
|
|
4575
|
+
|
|
4576
|
+
// iterate over the cluster voxels (value 2 in img) to perform dilation
|
|
4577
|
+
for (let idx = 0; idx < totalVoxels; idx++) {
|
|
4578
|
+
if (img[idx] === 2) {
|
|
4579
|
+
const x = idx % xDim
|
|
4580
|
+
const y = Math.floor((idx % nxy) / xDim)
|
|
4581
|
+
const z = Math.floor(idx / nxy)
|
|
4582
|
+
|
|
4583
|
+
for (const offset of neighborOffsets) {
|
|
4584
|
+
const neighborIdx = idx + offset
|
|
4585
|
+
|
|
4586
|
+
// skip if neighbor index is out of bounds
|
|
4587
|
+
if (neighborIdx < 0 || neighborIdx >= totalVoxels) {
|
|
4588
|
+
continue
|
|
4589
|
+
}
|
|
4590
|
+
|
|
4591
|
+
// calculate neighbor coordinates
|
|
4592
|
+
const nx = neighborIdx % xDim
|
|
4593
|
+
const ny = Math.floor((neighborIdx % nxy) / xDim)
|
|
4594
|
+
const nz = Math.floor(neighborIdx / nxy)
|
|
4595
|
+
|
|
4596
|
+
// ensure neighbor is adjacent (prevent wrapping around edges)
|
|
4597
|
+
if (Math.abs(nx - x) > 1 || Math.abs(ny - y) > 1 || Math.abs(nz - z) > 1) {
|
|
4598
|
+
continue
|
|
4599
|
+
}
|
|
4600
|
+
|
|
4601
|
+
// if the neighbor voxel is empty in the original mask, fill it in the output mask
|
|
4602
|
+
if (mask[neighborIdx] === 0) {
|
|
4603
|
+
outputMask[neighborIdx] = seedValue
|
|
4604
|
+
}
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
// set the output as the new drawing bitmap
|
|
4609
|
+
this.drawBitmap = outputMask
|
|
4610
|
+
// update the undo stack
|
|
4611
|
+
this.drawAddUndoBitmap()
|
|
4612
|
+
// refresh the drawing (copy from cpu to gpu) and show immediately
|
|
4613
|
+
this.refreshDrawing(true)
|
|
4614
|
+
} catch (error) {
|
|
4615
|
+
log.error('Error in drawingBinaryDilationWithSeed:', error)
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
|
|
4282
4619
|
// a voxel can be defined as having 6, 18 or 26 neighbors:
|
|
4283
4620
|
// 6: neighbors share faces (distance=1)
|
|
4284
4621
|
// 18: neighbors share faces (distance=1) or edges (1.4)
|
|
@@ -4381,7 +4718,10 @@ export class Niivue {
|
|
|
4381
4718
|
growSelectedCluster = 0, // if non-zero, growth based on background intensity POSITIVE_INFINITY for selected or bright, NEGATIVE_INFINITY for selected or darker
|
|
4382
4719
|
forceMin = NaN,
|
|
4383
4720
|
forceMax = NaN,
|
|
4384
|
-
neighbors = 6
|
|
4721
|
+
neighbors = 6,
|
|
4722
|
+
// option for only flood filling within max distance from seed voxel
|
|
4723
|
+
maxDistanceMM = Number.POSITIVE_INFINITY,
|
|
4724
|
+
is2D = false
|
|
4385
4725
|
): void {
|
|
4386
4726
|
if (!this.drawBitmap) {
|
|
4387
4727
|
throw new Error('drawBitmap undefined')
|
|
@@ -4403,14 +4743,55 @@ export class Niivue {
|
|
|
4403
4743
|
const nx = dims[0]
|
|
4404
4744
|
const nxy = nx * dims[1]
|
|
4405
4745
|
const nxyz = nxy * dims[2]
|
|
4406
|
-
|
|
4746
|
+
let img = this.drawBitmap.slice()
|
|
4747
|
+
let drawBitmap = this.drawBitmap
|
|
4748
|
+
// use the growing bitmap if the clickToSegmentIsGrowing flag is set.
|
|
4749
|
+
// this allows previewing the flood fill, and the results are copied
|
|
4750
|
+
// to the drawBitmap when the user releases the mouse button.
|
|
4751
|
+
if (this.clickToSegmentIsGrowing) {
|
|
4752
|
+
img = this.clickToSegmentGrowingBitmap.slice()
|
|
4753
|
+
drawBitmap = this.clickToSegmentGrowingBitmap
|
|
4754
|
+
}
|
|
4755
|
+
|
|
4407
4756
|
if (img.length !== nxy * dims[2]) {
|
|
4408
4757
|
return
|
|
4409
4758
|
}
|
|
4759
|
+
// mask2D: axial slices constrained in Z, coronal in Y and sagittal in X
|
|
4760
|
+
let constrainXYZ = -1
|
|
4761
|
+
if (is2D && this.drawPenAxCorSag === SLICE_TYPE.AXIAL) {
|
|
4762
|
+
constrainXYZ = 2
|
|
4763
|
+
} else if (is2D && this.drawPenAxCorSag === SLICE_TYPE.CORONAL) {
|
|
4764
|
+
constrainXYZ = 1
|
|
4765
|
+
} else if (is2D && this.drawPenAxCorSag === SLICE_TYPE.SAGITTAL) {
|
|
4766
|
+
constrainXYZ = 0
|
|
4767
|
+
}
|
|
4768
|
+
function vx2xyz(vx: number): number[] {
|
|
4769
|
+
// provided address in 1D array, return XYZ coordinate
|
|
4770
|
+
const Z = Math.floor(vx / nxy) // slice
|
|
4771
|
+
const Y = Math.floor((vx - Z * nxy) / nx) // column
|
|
4772
|
+
const X = Math.floor(vx % nx)
|
|
4773
|
+
return [X, Y, Z]
|
|
4774
|
+
}
|
|
4410
4775
|
function xyz2vx(pt: number[]): number {
|
|
4411
4776
|
// provided an XYZ 3D point, provide address in 1D array
|
|
4412
4777
|
return pt[0] + pt[1] * nx + pt[2] * nxy
|
|
4413
4778
|
}
|
|
4779
|
+
const vx2mm = (xyz: number[]): vec3 => {
|
|
4780
|
+
return this.vox2mm(xyz, this.back.matRAS)
|
|
4781
|
+
}
|
|
4782
|
+
// store seed vox as mm coordinates
|
|
4783
|
+
const seedMM = vx2mm(seedXYZ)
|
|
4784
|
+
const maxDistanceMM2 = maxDistanceMM ** 2
|
|
4785
|
+
// function to check if new point to be checked is less than maxDistanceMM
|
|
4786
|
+
function isWithinDistance(vx: number): boolean {
|
|
4787
|
+
const xzyVox = vx2xyz(vx)
|
|
4788
|
+
if (constrainXYZ >= 0 && xzyVox[constrainXYZ] !== seedXYZ[constrainXYZ]) {
|
|
4789
|
+
return false
|
|
4790
|
+
}
|
|
4791
|
+
const xyzMM = vx2mm(xzyVox)
|
|
4792
|
+
const dist2 = (xyzMM[0] - seedMM[0]) ** 2 + (xyzMM[1] - seedMM[1]) ** 2 + (xyzMM[2] - seedMM[2]) ** 2
|
|
4793
|
+
return dist2 <= maxDistanceMM2
|
|
4794
|
+
}
|
|
4414
4795
|
const seedVx = xyz2vx(seedXYZ)
|
|
4415
4796
|
const seedColor = img[seedVx]
|
|
4416
4797
|
if (seedColor === newColor) {
|
|
@@ -4423,7 +4804,13 @@ export class Niivue {
|
|
|
4423
4804
|
}
|
|
4424
4805
|
for (let i = 1; i < nxyz; i++) {
|
|
4425
4806
|
img[i] = 0
|
|
4426
|
-
if (
|
|
4807
|
+
if (drawBitmap[i] === seedColor) {
|
|
4808
|
+
// check if voxel index i is within maxDistanceMM from seed voxel
|
|
4809
|
+
if (!isWithinDistance(i)) {
|
|
4810
|
+
// move to next voxel if not within distance,
|
|
4811
|
+
// no need to check voxel intensity for cluster assignment
|
|
4812
|
+
continue
|
|
4813
|
+
}
|
|
4427
4814
|
img[i] = 1
|
|
4428
4815
|
}
|
|
4429
4816
|
}
|
|
@@ -4455,6 +4842,12 @@ export class Niivue {
|
|
|
4455
4842
|
for (let i = 1; i < nxyz; i++) {
|
|
4456
4843
|
img[i] = 0
|
|
4457
4844
|
if (backImg[i] >= mn && backImg[i] <= mx) {
|
|
4845
|
+
// check if voxel index i is within maxDistanceMM from seed voxel
|
|
4846
|
+
if (!isWithinDistance(i)) {
|
|
4847
|
+
// move to next voxel if not within distance,
|
|
4848
|
+
// no need to check voxel intensity for cluster assignment
|
|
4849
|
+
continue
|
|
4850
|
+
}
|
|
4458
4851
|
img[i] = 1
|
|
4459
4852
|
}
|
|
4460
4853
|
}
|
|
@@ -4465,11 +4858,22 @@ export class Niivue {
|
|
|
4465
4858
|
for (let i = 1; i < nxyz; i++) {
|
|
4466
4859
|
if (img[i] === 2) {
|
|
4467
4860
|
// if part of cluster
|
|
4468
|
-
|
|
4861
|
+
drawBitmap[i] = newColor
|
|
4469
4862
|
}
|
|
4470
4863
|
}
|
|
4471
|
-
this.
|
|
4472
|
-
|
|
4864
|
+
if (!this.clickToSegmentIsGrowing) {
|
|
4865
|
+
this.drawBitmap = drawBitmap.slice()
|
|
4866
|
+
if (!this.opts.clickToSegment) {
|
|
4867
|
+
// only update the undo array if clickToSegment is not enabled.
|
|
4868
|
+
// This avoids adding the same drawing twice, since the mouse up
|
|
4869
|
+
// event will also add the drawing to the undo array.
|
|
4870
|
+
this.drawAddUndoBitmap()
|
|
4871
|
+
}
|
|
4872
|
+
this.refreshDrawing(true, this.clickToSegmentIsGrowing)
|
|
4873
|
+
} else {
|
|
4874
|
+
this.clickToSegmentGrowingBitmap = drawBitmap
|
|
4875
|
+
this.refreshDrawing(true, this.clickToSegmentIsGrowing)
|
|
4876
|
+
}
|
|
4473
4877
|
}
|
|
4474
4878
|
|
|
4475
4879
|
// not included in public docs
|
|
@@ -4656,6 +5060,7 @@ export class Niivue {
|
|
|
4656
5060
|
this.drawClearAllUndoBitmaps()
|
|
4657
5061
|
this.rgbaTex(this.drawTexture, TEXTURE7_DRAW, [2, 2, 2, 2], true)
|
|
4658
5062
|
this.drawBitmap = null
|
|
5063
|
+
this.clickToSegmentGrowingBitmap = null
|
|
4659
5064
|
this.drawScene()
|
|
4660
5065
|
}
|
|
4661
5066
|
|
|
@@ -4665,7 +5070,7 @@ export class Niivue {
|
|
|
4665
5070
|
* @example niivue.refreshDrawing();
|
|
4666
5071
|
* @see {@link https://niivue.github.io/niivue/features/cactus.html | live demo usage}
|
|
4667
5072
|
*/
|
|
4668
|
-
refreshDrawing(isForceRedraw = true): void {
|
|
5073
|
+
refreshDrawing(isForceRedraw = true, useClickToSegmentBitmap = false): void {
|
|
4669
5074
|
if (!this.back?.dims) {
|
|
4670
5075
|
throw new Error('back.dims undefined')
|
|
4671
5076
|
}
|
|
@@ -4695,7 +5100,7 @@ export class Niivue {
|
|
|
4695
5100
|
dims[3],
|
|
4696
5101
|
this.gl.RED,
|
|
4697
5102
|
this.gl.UNSIGNED_BYTE,
|
|
4698
|
-
this.drawBitmap
|
|
5103
|
+
useClickToSegmentBitmap ? this.clickToSegmentGrowingBitmap : this.drawBitmap
|
|
4699
5104
|
)
|
|
4700
5105
|
if (isForceRedraw) {
|
|
4701
5106
|
this.drawScene()
|
|
@@ -5163,6 +5568,8 @@ export class Niivue {
|
|
|
5163
5568
|
// rect shader (crosshair): horizontal and vertical lines only
|
|
5164
5569
|
this.rectShader = new Shader(gl, vertRectShader, fragRectShader)
|
|
5165
5570
|
this.rectShader.use(gl)
|
|
5571
|
+
this.rectOutlineShader = new Shader(gl, vertRectShader, fragRectOutlineShader)
|
|
5572
|
+
this.rectOutlineShader.use(gl)
|
|
5166
5573
|
// line shader: diagonal lines
|
|
5167
5574
|
this.lineShader = new Shader(gl, vertLineShader, fragRectShader)
|
|
5168
5575
|
this.lineShader.use(gl)
|
|
@@ -5338,14 +5745,46 @@ export class Niivue {
|
|
|
5338
5745
|
|
|
5339
5746
|
/**
|
|
5340
5747
|
* basic statistics for selected voxel-based image
|
|
5341
|
-
* @param
|
|
5342
|
-
*
|
|
5343
|
-
*
|
|
5344
|
-
*
|
|
5748
|
+
* @param options - an object containing the following properties:
|
|
5749
|
+
* - layer: selects image to describe
|
|
5750
|
+
* - masks: optional binary images to filter voxels
|
|
5751
|
+
* - drawingIsMask: a boolean indicating if the drawing is used as a mask
|
|
5752
|
+
* - roiIsMask: a boolean indicating if the ROI is used as a mask
|
|
5753
|
+
* - startVox: the starting voxel coordinates
|
|
5754
|
+
* - endVox: the ending voxel coordinates
|
|
5755
|
+
* @returns numeric values to describe image or regions of images
|
|
5756
|
+
* @example
|
|
5757
|
+
* niivue.getDescriptives({
|
|
5758
|
+
* layer: 0,
|
|
5759
|
+
* masks: [],
|
|
5760
|
+
* drawingIsMask: true, // drawingIsMask and roiIsMask are mutually exclusive
|
|
5761
|
+
* roiIsMask: false,
|
|
5762
|
+
* startVox: [10, 20, 30], // ignored if roiIsMask is false
|
|
5763
|
+
* endVox: [40, 50, 60] // ignored if roiIsMask is false
|
|
5764
|
+
* });
|
|
5345
5765
|
* @see {@link https://niivue.github.io/niivue/features/draw2.html | live demo usage}
|
|
5346
5766
|
*/
|
|
5347
|
-
getDescriptives(
|
|
5767
|
+
getDescriptives(options: {
|
|
5768
|
+
layer?: number
|
|
5769
|
+
masks?: number[]
|
|
5770
|
+
drawingIsMask?: boolean
|
|
5771
|
+
roiIsMask?: boolean
|
|
5772
|
+
startVox?: number[]
|
|
5773
|
+
endVox?: number[]
|
|
5774
|
+
}): Descriptive {
|
|
5775
|
+
const {
|
|
5776
|
+
layer = 0,
|
|
5777
|
+
masks = [],
|
|
5778
|
+
drawingIsMask = false,
|
|
5779
|
+
roiIsMask = false,
|
|
5780
|
+
startVox = [0, 0, 0],
|
|
5781
|
+
endVox = [0, 0, 0]
|
|
5782
|
+
} = options
|
|
5783
|
+
|
|
5784
|
+
// Rest of the code remains the same
|
|
5785
|
+
let area = null // used if roiIsMask since ROI is in 2D slice
|
|
5348
5786
|
const hdr = this.volumes[layer].hdr!
|
|
5787
|
+
const pixDimsRAS = this.volumes[layer].pixDimsRAS!
|
|
5349
5788
|
let slope = hdr.scl_slope
|
|
5350
5789
|
if (isNaN(slope)) {
|
|
5351
5790
|
slope = 1
|
|
@@ -5384,6 +5823,104 @@ export class Niivue {
|
|
|
5384
5823
|
mask[i] = 0
|
|
5385
5824
|
}
|
|
5386
5825
|
}
|
|
5826
|
+
} else if (masks.length < 1 && roiIsMask) {
|
|
5827
|
+
// fill mask with zeros
|
|
5828
|
+
mask.fill(0)
|
|
5829
|
+
console.log('startVox', startVox)
|
|
5830
|
+
console.log('endVox', endVox)
|
|
5831
|
+
|
|
5832
|
+
// identify the constant dimension (the plane where the ellipse is drawn)
|
|
5833
|
+
let constantDim = -1
|
|
5834
|
+
if (startVox[0] === endVox[0]) {
|
|
5835
|
+
constantDim = 0 // x is constant
|
|
5836
|
+
} else if (startVox[1] === endVox[1]) {
|
|
5837
|
+
constantDim = 1 // y is constant
|
|
5838
|
+
} else if (startVox[2] === endVox[2]) {
|
|
5839
|
+
constantDim = 2 // z is constant
|
|
5840
|
+
} else {
|
|
5841
|
+
console.error('Error: No constant dimension found.')
|
|
5842
|
+
return
|
|
5843
|
+
}
|
|
5844
|
+
|
|
5845
|
+
// get the varying dimensions
|
|
5846
|
+
const dims = [0, 1, 2]
|
|
5847
|
+
const varDims = dims.filter((dim) => dim !== constantDim)
|
|
5848
|
+
|
|
5849
|
+
// compute the center of the ellipse in voxel coordinates
|
|
5850
|
+
const centerVox = []
|
|
5851
|
+
centerVox[constantDim] = startVox[constantDim]
|
|
5852
|
+
centerVox[varDims[0]] = (startVox[varDims[0]] + endVox[varDims[0]]) / 2
|
|
5853
|
+
centerVox[varDims[1]] = (startVox[varDims[1]] + endVox[varDims[1]]) / 2
|
|
5854
|
+
|
|
5855
|
+
// compute the radii along each varying dimension
|
|
5856
|
+
const radiusX = Math.abs(endVox[varDims[0]] - startVox[varDims[0]]) / 2
|
|
5857
|
+
const radiusY = Math.abs(endVox[varDims[1]] - startVox[varDims[1]]) / 2
|
|
5858
|
+
|
|
5859
|
+
// dimensions of the image
|
|
5860
|
+
const xdim = hdr.dims[1]
|
|
5861
|
+
const ydim = hdr.dims[2]
|
|
5862
|
+
// const zdim = hdr.dims[3]
|
|
5863
|
+
|
|
5864
|
+
// define the ranges for the varying dimensions
|
|
5865
|
+
const minVarDim0 = Math.max(0, Math.floor(centerVox[varDims[0]] - radiusX))
|
|
5866
|
+
const maxVarDim0 = Math.min(hdr.dims[varDims[0] + 1] - 1, Math.ceil(centerVox[varDims[0]] + radiusX))
|
|
5867
|
+
|
|
5868
|
+
const minVarDim1 = Math.max(0, Math.floor(centerVox[varDims[1]] - radiusY))
|
|
5869
|
+
const maxVarDim1 = Math.min(hdr.dims[varDims[1] + 1] - 1, Math.ceil(centerVox[varDims[1]] + radiusY))
|
|
5870
|
+
|
|
5871
|
+
// the constant dimension value
|
|
5872
|
+
const constDimVal = centerVox[constantDim]
|
|
5873
|
+
if (constDimVal < 0 || constDimVal >= hdr.dims[constantDim + 1]) {
|
|
5874
|
+
console.error('Error: Constant dimension value is out of bounds.')
|
|
5875
|
+
return
|
|
5876
|
+
}
|
|
5877
|
+
|
|
5878
|
+
// iterate over the varying dimensions and apply the elliptical mask
|
|
5879
|
+
for (let i = minVarDim0; i <= maxVarDim0; i++) {
|
|
5880
|
+
for (let j = minVarDim1; j <= maxVarDim1; j++) {
|
|
5881
|
+
// set the voxel coordinates
|
|
5882
|
+
const voxel = []
|
|
5883
|
+
voxel[constantDim] = constDimVal // Fixed dimension
|
|
5884
|
+
voxel[varDims[0]] = i
|
|
5885
|
+
voxel[varDims[1]] = j
|
|
5886
|
+
// calculate the normalized distances from the center
|
|
5887
|
+
const di = (voxel[varDims[0]] - centerVox[varDims[0]]) / radiusX
|
|
5888
|
+
const dj = (voxel[varDims[1]] - centerVox[varDims[1]]) / radiusY
|
|
5889
|
+
// calculate the squared distance in ellipse space
|
|
5890
|
+
const distSq = di * di + dj * dj
|
|
5891
|
+
// check if the voxel is within the ellipse
|
|
5892
|
+
if (distSq <= 1) {
|
|
5893
|
+
// calculate the index in the mask array
|
|
5894
|
+
const x = voxel[0]
|
|
5895
|
+
const y = voxel[1]
|
|
5896
|
+
const z = voxel[2]
|
|
5897
|
+
const index = z * xdim * ydim + y * xdim + x
|
|
5898
|
+
mask[index] = 1
|
|
5899
|
+
}
|
|
5900
|
+
}
|
|
5901
|
+
}
|
|
5902
|
+
// calculate the area based on the number of voxels in the mask
|
|
5903
|
+
const voxelArea = pixDimsRAS[varDims[0] + 1] * pixDimsRAS[varDims[1] + 1] // adjusted for 1-indexing
|
|
5904
|
+
const numMaskedVoxels = mask.reduce((count, value) => count + (value === 1 ? 1 : 0), 0)
|
|
5905
|
+
area = numMaskedVoxels * voxelArea
|
|
5906
|
+
|
|
5907
|
+
// perhaps better to calculate the area using the ellipse area formula
|
|
5908
|
+
const radiusX_mm = radiusX * pixDimsRAS[varDims[0] + 1]
|
|
5909
|
+
const radiusY_mm = radiusY * pixDimsRAS[varDims[1] + 1]
|
|
5910
|
+
const areaEllipse = Math.PI * radiusX_mm * radiusY_mm
|
|
5911
|
+
area = areaEllipse
|
|
5912
|
+
// for debuging: show mask -- loop over drawing and set drawing to 1 if mask is 1
|
|
5913
|
+
// this.setDrawingEnabled(true)
|
|
5914
|
+
// this.drawOpacity = 0.3
|
|
5915
|
+
// for (let i = 0; i < nv; i++) {
|
|
5916
|
+
// if (mask[i] === 1) {
|
|
5917
|
+
// this.drawBitmap![i] = 1
|
|
5918
|
+
// } else {
|
|
5919
|
+
// this.drawBitmap![i] = 0
|
|
5920
|
+
// }
|
|
5921
|
+
// }
|
|
5922
|
+
// this.refreshDrawing(false)
|
|
5923
|
+
// this.setDrawingEnabled(false)
|
|
5387
5924
|
}
|
|
5388
5925
|
// Welfords method
|
|
5389
5926
|
// https://www.embeddedrelated.com/showarticle/785.php
|
|
@@ -5444,7 +5981,8 @@ export class Niivue {
|
|
|
5444
5981
|
cal_min: this.volumes[layer].cal_min!,
|
|
5445
5982
|
cal_max: this.volumes[layer].cal_max!,
|
|
5446
5983
|
robust_min: this.volumes[layer].robust_min!,
|
|
5447
|
-
robust_max: this.volumes[layer].robust_max
|
|
5984
|
+
robust_max: this.volumes[layer].robust_max!,
|
|
5985
|
+
area
|
|
5448
5986
|
}
|
|
5449
5987
|
}
|
|
5450
5988
|
|
|
@@ -6796,6 +7334,7 @@ export class Niivue {
|
|
|
6796
7334
|
* @see {@link https://niivue.github.io/niivue/features/colormaps.html | live demo usage}
|
|
6797
7335
|
*/
|
|
6798
7336
|
setGamma(gamma = 1.0): void {
|
|
7337
|
+
this.scene.gamma = gamma
|
|
6799
7338
|
cmapper.gamma = gamma
|
|
6800
7339
|
this.updateGLVolume()
|
|
6801
7340
|
}
|
|
@@ -7114,6 +7653,31 @@ export class Niivue {
|
|
|
7114
7653
|
return pos[0] > 0 && pos[1] > 0 && pos[0] <= 1 && pos[1] <= 1
|
|
7115
7654
|
}
|
|
7116
7655
|
|
|
7656
|
+
// update drawBitmap if it differs from clickTosegmentBitmap
|
|
7657
|
+
updateBitmapFromClickToSegment(): void {
|
|
7658
|
+
if (this.clickToSegmentGrowingBitmap === null) {
|
|
7659
|
+
return
|
|
7660
|
+
}
|
|
7661
|
+
if (this.drawBitmap === null) {
|
|
7662
|
+
return
|
|
7663
|
+
}
|
|
7664
|
+
if (this.clickToSegmentGrowingBitmap.length !== this.drawBitmap.length) {
|
|
7665
|
+
return
|
|
7666
|
+
}
|
|
7667
|
+
const nvx = this.drawBitmap.length
|
|
7668
|
+
for (let i = 0; i < nvx; i++) {
|
|
7669
|
+
this.drawBitmap[i] = this.clickToSegmentGrowingBitmap[i]
|
|
7670
|
+
}
|
|
7671
|
+
}
|
|
7672
|
+
|
|
7673
|
+
sumBitmap(img: Uint8Array): number {
|
|
7674
|
+
let sum = 0
|
|
7675
|
+
for (let i = 0; i < img.length; i++) {
|
|
7676
|
+
sum += img[i]
|
|
7677
|
+
}
|
|
7678
|
+
return sum
|
|
7679
|
+
}
|
|
7680
|
+
|
|
7117
7681
|
// not included in public docs
|
|
7118
7682
|
// handle mouse click event on canvas
|
|
7119
7683
|
mouseClick(x: number, y: number, posChange = 0, isDelta = true): void {
|
|
@@ -7177,7 +7741,7 @@ export class Niivue {
|
|
|
7177
7741
|
if (axCorSag > SLICE_TYPE.SAGITTAL) {
|
|
7178
7742
|
continue
|
|
7179
7743
|
}
|
|
7180
|
-
|
|
7744
|
+
let texFrac = this.screenXY2TextureFrac(x, y, i, false)
|
|
7181
7745
|
if (texFrac[0] < 0) {
|
|
7182
7746
|
continue
|
|
7183
7747
|
} // click not on slice i
|
|
@@ -7207,34 +7771,102 @@ export class Niivue {
|
|
|
7207
7771
|
this.scene.crosshairPos = vec3.clone(texFrac)
|
|
7208
7772
|
}
|
|
7209
7773
|
if (this.opts.drawingEnabled) {
|
|
7210
|
-
|
|
7774
|
+
// drawing done in voxels
|
|
7775
|
+
let pt = this.frac2vox(this.scene.crosshairPos) as [number, number, number]
|
|
7211
7776
|
// if click-to-segment enabled
|
|
7212
7777
|
if (this.opts.clickToSegment) {
|
|
7213
|
-
|
|
7214
|
-
|
|
7778
|
+
texFrac = this.screenXY2TextureFrac(this.clickToSegmentXY[0], this.clickToSegmentXY[1], i, false)
|
|
7779
|
+
pt = this.frac2vox(texFrac) as [number, number, number]
|
|
7780
|
+
let diff = 0
|
|
7781
|
+
let threshold = this.opts.clickToSegmentPercent + diff
|
|
7782
|
+
if (this.uiData.mousedown) {
|
|
7783
|
+
// get percent difference between current x, y, and clickToSegmentXY
|
|
7784
|
+
const xDiff = (this.clickToSegmentXY[0] - x) / this.gl.canvas.width
|
|
7785
|
+
const yDiff = (this.clickToSegmentXY[1] - y) / this.gl.canvas.height
|
|
7786
|
+
diff = Math.max(Math.abs(xDiff), Math.abs(yDiff))
|
|
7787
|
+
// use the sign of the larger difference to determine the direction
|
|
7788
|
+
diff *= Math.sign(Math.abs(xDiff) > Math.abs(yDiff) ? xDiff : yDiff)
|
|
7789
|
+
threshold = this.opts.clickToSegmentPercent + diff
|
|
7790
|
+
}
|
|
7791
|
+
// get voxel value of pt
|
|
7792
|
+
let voxelIntensity = this.back.getValue(pt[0], pt[1], pt[2])
|
|
7793
|
+
// if clickToSegmentAutoIntensity, then calculate if we need to flood fill
|
|
7794
|
+
// in a bright or dark region based on the intensity of the clicked voxel,
|
|
7795
|
+
// and where it falls in the range of cal_min and cal_max.
|
|
7796
|
+
// !important! If this option is true, then it will ignore the boolean value of
|
|
7797
|
+
// clickToSegmentBright supplied by the user
|
|
7798
|
+
if (this.opts.clickToSegmentAutoIntensity) {
|
|
7799
|
+
if (threshold !== 0) {
|
|
7800
|
+
if (voxelIntensity === 0) {
|
|
7801
|
+
voxelIntensity = 0.01
|
|
7802
|
+
}
|
|
7803
|
+
this.opts.clickToSegmentIntensityMax = voxelIntensity * (1 + threshold)
|
|
7804
|
+
this.opts.clickToSegmentIntensityMin = voxelIntensity * (1 - threshold)
|
|
7805
|
+
}
|
|
7806
|
+
// if voxel intensity is greater than the midpoint of cal_min and cal_max,
|
|
7807
|
+
// then flood fill in a bright region
|
|
7808
|
+
if (voxelIntensity > (this.back.cal_min + this.back.cal_max) * 0.5) {
|
|
7809
|
+
this.opts.clickToSegmentBright = true
|
|
7810
|
+
} else {
|
|
7811
|
+
// else flood fill in a dark region
|
|
7812
|
+
this.opts.clickToSegmentBright = false
|
|
7813
|
+
}
|
|
7814
|
+
}
|
|
7815
|
+
// set brightOrDark now, if clickToSegmentAutoBrightOrDark is false,
|
|
7816
|
+
// then brightOrDark will be set to the value of clickToSegmentBright supplied by the user
|
|
7215
7817
|
const brightOrDark = this.opts.clickToSegmentBright ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY
|
|
7216
|
-
this.drawPenFillPts = []
|
|
7217
7818
|
this.drawPenAxCorSag = axCorSag
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
const
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
this.
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7819
|
+
if (this.drawPenAxCorSag === SLICE_TYPE.AXIAL) {
|
|
7820
|
+
// get voxel penSize for the current slice, but what if pixDims are not isotropic?
|
|
7821
|
+
// we will use the smallest pixDim for the current slice
|
|
7822
|
+
const pixDims = [this.back.pixDimsRAS[0], this.back.pixDimsRAS[1]]
|
|
7823
|
+
const minPixDim = Math.min(...pixDims)
|
|
7824
|
+
this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim)
|
|
7825
|
+
} else if (this.drawPenAxCorSag === SLICE_TYPE.CORONAL) {
|
|
7826
|
+
const pixDims = [this.back.pixDimsRAS[0], this.back.pixDimsRAS[2]]
|
|
7827
|
+
const minPixDim = Math.min(...pixDims)
|
|
7828
|
+
this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim)
|
|
7829
|
+
} else if (this.drawPenAxCorSag === SLICE_TYPE.SAGITTAL) {
|
|
7830
|
+
const pixDims = [this.back.pixDimsRAS[1], this.back.pixDimsRAS[2]]
|
|
7831
|
+
const minPixDim = Math.min(...pixDims)
|
|
7832
|
+
this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim)
|
|
7833
|
+
}
|
|
7834
|
+
// draw the point at the cursor using the penSize to set the
|
|
7835
|
+
// seed for the flood fill
|
|
7836
|
+
this.drawPt(pt[0], pt[1], pt[2], this.opts.penValue)
|
|
7837
|
+
|
|
7838
|
+
if (diff !== 0) {
|
|
7839
|
+
this.clickToSegmentIsGrowing = true
|
|
7840
|
+
this.clickToSegmentGrowingBitmap = this.drawBitmap.slice()
|
|
7841
|
+
} else {
|
|
7842
|
+
this.clickToSegmentIsGrowing = false
|
|
7233
7843
|
}
|
|
7844
|
+
|
|
7845
|
+
// do flood fill
|
|
7846
|
+
this.drawFloodFill(
|
|
7847
|
+
[pt[0], pt[1], pt[2]],
|
|
7848
|
+
0,
|
|
7849
|
+
brightOrDark,
|
|
7850
|
+
this.opts.clickToSegmentIntensityMin,
|
|
7851
|
+
this.opts.clickToSegmentIntensityMax,
|
|
7852
|
+
this.opts.floodFillNeighbors,
|
|
7853
|
+
this.opts.clickToSegmentMaxDistanceMM,
|
|
7854
|
+
this.opts.clickToSegmentIs2D
|
|
7855
|
+
)
|
|
7856
|
+
// draw the scene so we see the changes
|
|
7234
7857
|
this.drawScene()
|
|
7235
7858
|
this.createOnLocationChange(axCorSag)
|
|
7236
|
-
|
|
7237
|
-
|
|
7859
|
+
if (this.clickToSegmentIsGrowing) {
|
|
7860
|
+
// don't get descriptive stats if the user
|
|
7861
|
+
// is still growing the segmented region
|
|
7862
|
+
return
|
|
7863
|
+
}
|
|
7864
|
+
// get the volume of the segmented region if the user is not growing the region
|
|
7865
|
+
const info = this.getDescriptives({
|
|
7866
|
+
layer: 0,
|
|
7867
|
+
masks: [],
|
|
7868
|
+
drawingIsMask: true
|
|
7869
|
+
})
|
|
7238
7870
|
this.onClickToSegment({ mL: info.volumeML, mm3: info.volumeMM3 })
|
|
7239
7871
|
return
|
|
7240
7872
|
}
|
|
@@ -7486,20 +8118,44 @@ export class Niivue {
|
|
|
7486
8118
|
if (!this.rectShader) {
|
|
7487
8119
|
throw new Error('rectShader undefined')
|
|
7488
8120
|
}
|
|
7489
|
-
this.
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
this.
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
8121
|
+
if (!this.opts.selectionBoxIsOutline) {
|
|
8122
|
+
this.rectShader.use(this.gl)
|
|
8123
|
+
this.gl.enable(this.gl.BLEND)
|
|
8124
|
+
this.gl.uniform4fv(this.rectShader.uniforms.lineColor, lineColor)
|
|
8125
|
+
this.gl.uniform2fv(this.rectShader.uniforms.canvasWidthHeight, [this.gl.canvas.width, this.gl.canvas.height])
|
|
8126
|
+
this.gl.uniform4f(
|
|
8127
|
+
this.rectShader.uniforms.leftTopWidthHeight,
|
|
8128
|
+
leftTopWidthHeight[0],
|
|
8129
|
+
leftTopWidthHeight[1],
|
|
8130
|
+
leftTopWidthHeight[2],
|
|
8131
|
+
leftTopWidthHeight[3]
|
|
8132
|
+
)
|
|
8133
|
+
this.gl.bindVertexArray(this.genericVAO)
|
|
8134
|
+
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4)
|
|
8135
|
+
this.gl.bindVertexArray(this.unusedVAO) // switch off to avoid tampering with settings
|
|
8136
|
+
} else {
|
|
8137
|
+
this.drawCircle(leftTopWidthHeight, lineColor, 0.1)
|
|
8138
|
+
// this.opts.selectionBoxIsOutline == true
|
|
8139
|
+
this.rectOutlineShader.use(this.gl)
|
|
8140
|
+
this.gl.enable(this.gl.BLEND)
|
|
8141
|
+
// set thickness of line
|
|
8142
|
+
this.gl.uniform1f(this.rectOutlineShader.uniforms.thickness, this.opts.selectionBoxLineThickness)
|
|
8143
|
+
this.gl.uniform4fv(this.rectOutlineShader.uniforms.lineColor, lineColor)
|
|
8144
|
+
this.gl.uniform2fv(this.rectOutlineShader.uniforms.canvasWidthHeight, [
|
|
8145
|
+
this.gl.canvas.width,
|
|
8146
|
+
this.gl.canvas.height
|
|
8147
|
+
])
|
|
8148
|
+
this.gl.uniform4f(
|
|
8149
|
+
this.rectOutlineShader.uniforms.leftTopWidthHeight,
|
|
8150
|
+
leftTopWidthHeight[0],
|
|
8151
|
+
leftTopWidthHeight[1],
|
|
8152
|
+
leftTopWidthHeight[2],
|
|
8153
|
+
leftTopWidthHeight[3]
|
|
8154
|
+
)
|
|
8155
|
+
this.gl.bindVertexArray(this.genericVAO)
|
|
8156
|
+
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4)
|
|
8157
|
+
this.gl.bindVertexArray(this.unusedVAO) // switch off to avoid tampering with settings
|
|
8158
|
+
}
|
|
7503
8159
|
}
|
|
7504
8160
|
|
|
7505
8161
|
drawCircle(leftTopWidthHeight: number[], circleColor = this.opts.fontColor, fillPercent = 1.0): void {
|
|
@@ -7527,6 +8183,11 @@ export class Niivue {
|
|
|
7527
8183
|
// not included in public docs
|
|
7528
8184
|
// draw a rectangle at desired location
|
|
7529
8185
|
drawSelectionBox(leftTopWidthHeight: number[]): void {
|
|
8186
|
+
if (this.opts.dragMode === DRAG_MODE.roiSelection) {
|
|
8187
|
+
this.drawCircle(leftTopWidthHeight, this.opts.selectionBoxColor, 0.1)
|
|
8188
|
+
return
|
|
8189
|
+
}
|
|
8190
|
+
// else draw a rectangle
|
|
7530
8191
|
this.drawRect(leftTopWidthHeight, this.opts.selectionBoxColor)
|
|
7531
8192
|
}
|
|
7532
8193
|
|
|
@@ -7551,9 +8212,26 @@ export class Niivue {
|
|
|
7551
8212
|
return labels
|
|
7552
8213
|
}
|
|
7553
8214
|
|
|
8215
|
+
getConnectomeLabels(): NVLabel3D[] {
|
|
8216
|
+
const connectomes = this.meshes.filter((m) => m.type === MeshType.CONNECTOME)
|
|
8217
|
+
const meshNodes = connectomes.flatMap((m) => m.nodes as NVConnectomeNode[])
|
|
8218
|
+
const meshLabels = meshNodes.map((n) => n.label)
|
|
8219
|
+
// filter our undefined labels
|
|
8220
|
+
const definedMeshLabels = meshLabels.filter((l): l is NVLabel3D => l !== undefined)
|
|
8221
|
+
// get all of our non-anchored labels
|
|
8222
|
+
const nonAnchoredLabels = this.document.labels.filter((l) => l.anchor == null || l.anchor === LabelAnchorPoint.NONE)
|
|
8223
|
+
// get the unique set of unanchored labels
|
|
8224
|
+
const nonAnchoredLabelSet = new Set(definedMeshLabels)
|
|
8225
|
+
for (const label of nonAnchoredLabels) {
|
|
8226
|
+
nonAnchoredLabelSet.add(label)
|
|
8227
|
+
}
|
|
8228
|
+
|
|
8229
|
+
return Array.from(nonAnchoredLabelSet)
|
|
8230
|
+
}
|
|
8231
|
+
|
|
7554
8232
|
getBulletMarginWidth(): number {
|
|
7555
8233
|
let bulletMargin = 0
|
|
7556
|
-
const labels = this.
|
|
8234
|
+
const labels = this.getConnectomeLabels()
|
|
7557
8235
|
if (labels.length === 0) {
|
|
7558
8236
|
return 0
|
|
7559
8237
|
}
|
|
@@ -7578,7 +8256,7 @@ export class Niivue {
|
|
|
7578
8256
|
}
|
|
7579
8257
|
|
|
7580
8258
|
getLegendPanelWidth(): number {
|
|
7581
|
-
const labels = this.
|
|
8259
|
+
const labels = this.getConnectomeLabels()
|
|
7582
8260
|
if (!this.opts.showLegend || labels.length === 0) {
|
|
7583
8261
|
return 0
|
|
7584
8262
|
}
|
|
@@ -7609,7 +8287,7 @@ export class Niivue {
|
|
|
7609
8287
|
}
|
|
7610
8288
|
|
|
7611
8289
|
getLegendPanelHeight(): number {
|
|
7612
|
-
const labels = this.
|
|
8290
|
+
const labels = this.getConnectomeLabels()
|
|
7613
8291
|
let height = 0
|
|
7614
8292
|
const scale = 1.0 // we may want to make this adjustable in the future
|
|
7615
8293
|
const verticalMargin = this.opts.textHeight * this.gl.canvas.height * scale
|
|
@@ -8124,7 +8802,11 @@ export class Niivue {
|
|
|
8124
8802
|
|
|
8125
8803
|
// not included in public docs
|
|
8126
8804
|
// show text labels for L/R, A/P, I/S dimensions
|
|
8127
|
-
drawSliceOrientationText(
|
|
8805
|
+
drawSliceOrientationText(
|
|
8806
|
+
leftTopWidthHeight: number[],
|
|
8807
|
+
axCorSag: SLICE_TYPE,
|
|
8808
|
+
padLeftTop: number[] = [NaN, NaN]
|
|
8809
|
+
): void {
|
|
8128
8810
|
this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height)
|
|
8129
8811
|
let topText = 'S'
|
|
8130
8812
|
if (axCorSag === SLICE_TYPE.AXIAL) {
|
|
@@ -8138,9 +8820,32 @@ export class Niivue {
|
|
|
8138
8820
|
this.drawTextRightBelow([leftTopWidthHeight[0], leftTopWidthHeight[1]], leftText + topText)
|
|
8139
8821
|
return
|
|
8140
8822
|
}
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8823
|
+
let drawBelow = true
|
|
8824
|
+
let drawRight = true
|
|
8825
|
+
if (!isNaN(padLeftTop[0])) {
|
|
8826
|
+
const ht = this.opts.textHeight * this.gl.canvas.height + 2
|
|
8827
|
+
if (padLeftTop[1] > ht) {
|
|
8828
|
+
this.drawTextBelow(
|
|
8829
|
+
[leftTopWidthHeight[0] + leftTopWidthHeight[2] * 0.5, leftTopWidthHeight[1] + padLeftTop[1] - ht],
|
|
8830
|
+
topText
|
|
8831
|
+
)
|
|
8832
|
+
drawBelow = false
|
|
8833
|
+
}
|
|
8834
|
+
const wid = this.textWidth(ht, leftText) + 2
|
|
8835
|
+
if (padLeftTop[0] > wid) {
|
|
8836
|
+
this.drawTextRight(
|
|
8837
|
+
[leftTopWidthHeight[0] + padLeftTop[0] - wid, leftTopWidthHeight[1] + leftTopWidthHeight[3] * 0.5],
|
|
8838
|
+
leftText
|
|
8839
|
+
)
|
|
8840
|
+
drawRight = false
|
|
8841
|
+
}
|
|
8842
|
+
}
|
|
8843
|
+
if (drawBelow) {
|
|
8844
|
+
this.drawTextBelow([leftTopWidthHeight[0] + leftTopWidthHeight[2] * 0.5, leftTopWidthHeight[1]], topText)
|
|
8845
|
+
}
|
|
8846
|
+
if (drawRight) {
|
|
8847
|
+
this.drawTextRight([leftTopWidthHeight[0], leftTopWidthHeight[1] + leftTopWidthHeight[3] * 0.5], leftText)
|
|
8848
|
+
}
|
|
8144
8849
|
}
|
|
8145
8850
|
|
|
8146
8851
|
// not included in public docs
|
|
@@ -8183,7 +8888,7 @@ export class Niivue {
|
|
|
8183
8888
|
|
|
8184
8889
|
// not included in public docs
|
|
8185
8890
|
// draw 2D tile
|
|
8186
|
-
|
|
8891
|
+
draw2DMain(leftTopWidthHeight: number[], axCorSag: SLICE_TYPE, customMM = NaN): void {
|
|
8187
8892
|
let frac2mmTexture = this.volumes[0].frac2mm!.slice()
|
|
8188
8893
|
let screen = this.screenFieldOfViewExtendedMM(axCorSag)
|
|
8189
8894
|
let mesh2ortho = mat4.create()
|
|
@@ -8212,8 +8917,10 @@ export class Niivue {
|
|
|
8212
8917
|
elevation = isRadiolgical ? -90 : 90
|
|
8213
8918
|
}
|
|
8214
8919
|
const gl = this.gl
|
|
8920
|
+
let isStretchToScreen = false
|
|
8215
8921
|
if (leftTopWidthHeight[2] === 0 || leftTopWidthHeight[3] === 0) {
|
|
8216
8922
|
// only one tile: stretch tile to fill whole screen.
|
|
8923
|
+
isStretchToScreen = true
|
|
8217
8924
|
const pixPerMMw = gl.canvas.width / screen.fovMM[0]
|
|
8218
8925
|
const pixPerMMh = gl.canvas.height / screen.fovMM[1]
|
|
8219
8926
|
const pixPerMMmin = Math.min(pixPerMMw, pixPerMMh)
|
|
@@ -8372,10 +9079,42 @@ export class Niivue {
|
|
|
8372
9079
|
// no crossbars for mosaic view
|
|
8373
9080
|
this.drawCrosshairs3D(false, 0.15, obj.modelViewProjectionMatrix, true, this.opts.isSliceMM)
|
|
8374
9081
|
}
|
|
8375
|
-
|
|
9082
|
+
if (isStretchToScreen) {
|
|
9083
|
+
// issue1065
|
|
9084
|
+
this.drawSliceOrientationText(leftTopWidthHeight, axCorSag)
|
|
9085
|
+
}
|
|
8376
9086
|
this.readyForSync = true
|
|
8377
9087
|
}
|
|
8378
9088
|
|
|
9089
|
+
draw2D(
|
|
9090
|
+
leftTopWidthHeight: number[],
|
|
9091
|
+
axCorSag: SLICE_TYPE,
|
|
9092
|
+
customMM = NaN,
|
|
9093
|
+
imageWidthHeight: number[] = [NaN, NaN]
|
|
9094
|
+
): void {
|
|
9095
|
+
const padLeftTop = [NaN, NaN]
|
|
9096
|
+
if (isNaN(imageWidthHeight[0])) {
|
|
9097
|
+
this.draw2DMain(leftTopWidthHeight, axCorSag, customMM)
|
|
9098
|
+
} else {
|
|
9099
|
+
// inset as padded in tile
|
|
9100
|
+
const ltwh = leftTopWidthHeight.slice()
|
|
9101
|
+
padLeftTop[0] = Math.floor(0.5 * (ltwh[2] - imageWidthHeight[0]))
|
|
9102
|
+
padLeftTop[1] = Math.floor(0.5 * (ltwh[3] - imageWidthHeight[1]))
|
|
9103
|
+
ltwh[0] += padLeftTop[0]
|
|
9104
|
+
ltwh[1] += padLeftTop[1]
|
|
9105
|
+
ltwh[2] = imageWidthHeight[0]
|
|
9106
|
+
ltwh[3] = imageWidthHeight[1]
|
|
9107
|
+
this.draw2DMain(ltwh, axCorSag, customMM)
|
|
9108
|
+
}
|
|
9109
|
+
if (customMM === Infinity || customMM === -Infinity || axCorSag === SLICE_TYPE.RENDER) {
|
|
9110
|
+
return
|
|
9111
|
+
}
|
|
9112
|
+
if (leftTopWidthHeight[2] !== 0 && leftTopWidthHeight[3] !== 0) {
|
|
9113
|
+
// issue1065
|
|
9114
|
+
this.drawSliceOrientationText(leftTopWidthHeight, axCorSag, padLeftTop)
|
|
9115
|
+
}
|
|
9116
|
+
}
|
|
9117
|
+
|
|
8379
9118
|
// not included in public docs
|
|
8380
9119
|
// determine 3D model view projection matrix
|
|
8381
9120
|
calculateMvpMatrix(_unused: unknown, leftTopWidthHeight = [0, 0, 0, 0], azimuth: number, elevation: number): mat4[] {
|
|
@@ -9074,7 +9813,13 @@ export class Niivue {
|
|
|
9074
9813
|
* @param style - label style
|
|
9075
9814
|
* @param point - 3D point on the model
|
|
9076
9815
|
*/
|
|
9077
|
-
addLabel(
|
|
9816
|
+
addLabel(
|
|
9817
|
+
text: string,
|
|
9818
|
+
style: NVLabel3DStyle,
|
|
9819
|
+
points?: number[] | number[][],
|
|
9820
|
+
anchor?: LabelAnchorPoint,
|
|
9821
|
+
onClick?: (label: NVLabel3D) => void
|
|
9822
|
+
): NVLabel3D {
|
|
9078
9823
|
const defaultStyle = {
|
|
9079
9824
|
textColor: this.opts.legendTextColor,
|
|
9080
9825
|
textScale: 1.0,
|
|
@@ -9086,7 +9831,7 @@ export class Niivue {
|
|
|
9086
9831
|
bulletColor: this.opts.legendTextColor
|
|
9087
9832
|
}
|
|
9088
9833
|
const labelStyle = style ? { ...defaultStyle, ...style } : { ...defaultStyle }
|
|
9089
|
-
const label = new NVLabel3D(text, labelStyle, points)
|
|
9834
|
+
const label = new NVLabel3D(text, { ...labelStyle }, points, anchor, onClick)
|
|
9090
9835
|
this.document.labels.push(label)
|
|
9091
9836
|
return label
|
|
9092
9837
|
}
|
|
@@ -9109,6 +9854,75 @@ export class Niivue {
|
|
|
9109
9854
|
}
|
|
9110
9855
|
|
|
9111
9856
|
getLabelAtPoint(screenPoint: [number, number]): NVLabel3D | null {
|
|
9857
|
+
const scale = 1.0
|
|
9858
|
+
const size = this.opts.textHeight * Math.min(this.gl.canvas.height, this.gl.canvas.width) * scale
|
|
9859
|
+
const verticalMargin = this.opts.textHeight * this.gl.canvas.height * scale
|
|
9860
|
+
|
|
9861
|
+
// get all non-connectome labels
|
|
9862
|
+
for (const label of this.document.labels) {
|
|
9863
|
+
if (label.anchor == null || label.anchor === LabelAnchorPoint.NONE) {
|
|
9864
|
+
continue
|
|
9865
|
+
}
|
|
9866
|
+
|
|
9867
|
+
const labelSize = this.opts.textHeight * this.gl.canvas.height * label.style.textScale
|
|
9868
|
+
const textHeight = this.textHeight(labelSize, label.text)
|
|
9869
|
+
const textWidth = this.textWidth(labelSize, label.text)
|
|
9870
|
+
|
|
9871
|
+
if (label.anchor & LabelAnchorFlag.LEFT) {
|
|
9872
|
+
if (screenPoint[0] > textWidth) {
|
|
9873
|
+
continue
|
|
9874
|
+
}
|
|
9875
|
+
}
|
|
9876
|
+
|
|
9877
|
+
if (label.anchor & LabelAnchorFlag.CENTER) {
|
|
9878
|
+
if (screenPoint[0] < (this.gl.canvas.width - textWidth) / 2) {
|
|
9879
|
+
continue
|
|
9880
|
+
}
|
|
9881
|
+
|
|
9882
|
+
if (screenPoint[0] > (this.gl.canvas.width + textWidth) / 2) {
|
|
9883
|
+
continue
|
|
9884
|
+
}
|
|
9885
|
+
}
|
|
9886
|
+
|
|
9887
|
+
if (label.anchor & LabelAnchorFlag.RIGHT) {
|
|
9888
|
+
if (screenPoint[0] < this.gl.canvas.width - textWidth) {
|
|
9889
|
+
continue
|
|
9890
|
+
}
|
|
9891
|
+
}
|
|
9892
|
+
|
|
9893
|
+
if (label.anchor & LabelAnchorFlag.TOP) {
|
|
9894
|
+
if (screenPoint[1] < verticalMargin / 2) {
|
|
9895
|
+
continue
|
|
9896
|
+
}
|
|
9897
|
+
|
|
9898
|
+
if (screenPoint[1] > textHeight + verticalMargin / 2) {
|
|
9899
|
+
continue
|
|
9900
|
+
}
|
|
9901
|
+
}
|
|
9902
|
+
|
|
9903
|
+
if (label.anchor & LabelAnchorFlag.MIDDLE) {
|
|
9904
|
+
if (screenPoint[1] < (this.gl.canvas.height - textHeight - verticalMargin) / 2) {
|
|
9905
|
+
continue
|
|
9906
|
+
}
|
|
9907
|
+
|
|
9908
|
+
if (screenPoint[1] > (this.gl.canvas.height + textHeight - verticalMargin / 2) / 2) {
|
|
9909
|
+
continue
|
|
9910
|
+
}
|
|
9911
|
+
}
|
|
9912
|
+
|
|
9913
|
+
if (label.anchor & LabelAnchorFlag.BOTTOM) {
|
|
9914
|
+
if (screenPoint[1] < this.gl.canvas.height - textHeight - verticalMargin) {
|
|
9915
|
+
continue
|
|
9916
|
+
}
|
|
9917
|
+
|
|
9918
|
+
if (screenPoint[1] > this.gl.canvas.height - verticalMargin / 2) {
|
|
9919
|
+
continue
|
|
9920
|
+
}
|
|
9921
|
+
}
|
|
9922
|
+
|
|
9923
|
+
// label passed all tests
|
|
9924
|
+
return label
|
|
9925
|
+
}
|
|
9112
9926
|
log.debug('screenPoint', screenPoint)
|
|
9113
9927
|
const panelHeight = this.getLegendPanelHeight()
|
|
9114
9928
|
const panelWidth = this.getLegendPanelWidth()
|
|
@@ -9124,10 +9938,7 @@ export class Niivue {
|
|
|
9124
9938
|
return null
|
|
9125
9939
|
}
|
|
9126
9940
|
|
|
9127
|
-
const
|
|
9128
|
-
const size = this.opts.textHeight * Math.min(this.gl.canvas.height, this.gl.canvas.width) * scale
|
|
9129
|
-
|
|
9130
|
-
const labels = this.getAllLabels()
|
|
9941
|
+
const labels = this.getConnectomeLabels()
|
|
9131
9942
|
for (const label of labels) {
|
|
9132
9943
|
const labelSize = this.opts.textHeight * this.gl.canvas.height * label.style.textScale
|
|
9133
9944
|
const textHeight = this.textHeight(labelSize, label.text)
|
|
@@ -9165,11 +9976,11 @@ export class Niivue {
|
|
|
9165
9976
|
draw3DLabel(
|
|
9166
9977
|
label: NVLabel3D,
|
|
9167
9978
|
pos: vec2,
|
|
9168
|
-
mvpMatrix
|
|
9169
|
-
leftTopWidthHeight
|
|
9170
|
-
bulletMargin
|
|
9171
|
-
legendWidth
|
|
9172
|
-
secondPass
|
|
9979
|
+
mvpMatrix?: mat4,
|
|
9980
|
+
leftTopWidthHeight?: number[],
|
|
9981
|
+
bulletMargin?: number,
|
|
9982
|
+
legendWidth?: number,
|
|
9983
|
+
secondPass?: boolean
|
|
9173
9984
|
): void {
|
|
9174
9985
|
const text = label.text
|
|
9175
9986
|
const left = pos[0]
|
|
@@ -9195,17 +10006,19 @@ export class Niivue {
|
|
|
9195
10006
|
|
|
9196
10007
|
let textLeft = left
|
|
9197
10008
|
|
|
9198
|
-
if (
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
|
|
10009
|
+
if (legendWidth) {
|
|
10010
|
+
if (label.style.textAlignment !== LabelTextAlignment.LEFT) {
|
|
10011
|
+
const textWidth = this.textWidth(label.style.textScale, label.text) * size
|
|
10012
|
+
if (label.style.textAlignment === LabelTextAlignment.RIGHT) {
|
|
10013
|
+
textLeft = left + legendWidth - size * 1.5 - textWidth
|
|
10014
|
+
} else {
|
|
10015
|
+
const remaining = legendWidth - (bulletMargin || size)
|
|
10016
|
+
textLeft += (remaining - textWidth) / 2
|
|
10017
|
+
}
|
|
9202
10018
|
} else {
|
|
9203
|
-
|
|
9204
|
-
textLeft +=
|
|
10019
|
+
// textLeft += size / 2;
|
|
10020
|
+
textLeft += bulletMargin
|
|
9205
10021
|
}
|
|
9206
|
-
} else {
|
|
9207
|
-
// textLeft += size / 2;
|
|
9208
|
-
textLeft += bulletMargin
|
|
9209
10022
|
}
|
|
9210
10023
|
|
|
9211
10024
|
this.drawText([textLeft, top], text, label.style.textScale, label.style.textColor)
|
|
@@ -9213,7 +10026,7 @@ export class Niivue {
|
|
|
9213
10026
|
|
|
9214
10027
|
// not included in public docs
|
|
9215
10028
|
draw3DLabels(mvpMatrix: mat4, leftTopWidthHeight: number[], secondPass = false): void {
|
|
9216
|
-
const labels = this.
|
|
10029
|
+
const labels = this.getConnectomeLabels()
|
|
9217
10030
|
if (!this.opts.showLegend || labels.length === 0) {
|
|
9218
10031
|
return
|
|
9219
10032
|
}
|
|
@@ -9263,6 +10076,59 @@ export class Niivue {
|
|
|
9263
10076
|
}
|
|
9264
10077
|
}
|
|
9265
10078
|
|
|
10079
|
+
drawAnchoredLabels(): void {
|
|
10080
|
+
const size = this.opts.textHeight * Math.min(this.gl.canvas.height, this.gl.canvas.width) * 1.0
|
|
10081
|
+
const anchoredLabels = this.document.labels.filter((l) => l.anchor != null && l.anchor !== LabelAnchorPoint.NONE)
|
|
10082
|
+
for (const label of anchoredLabels) {
|
|
10083
|
+
const text = label.text
|
|
10084
|
+
const textHeight = this.textHeight(label.style.textScale, text) * size
|
|
10085
|
+
const textWidth = this.textWidth(label.style.textScale, text) * size
|
|
10086
|
+
let left: number
|
|
10087
|
+
let top: number
|
|
10088
|
+
|
|
10089
|
+
const scale = 1.0 // we may want to make this adjustable in the future
|
|
10090
|
+
const verticalMargin = this.opts.textHeight * this.gl.canvas.height * scale
|
|
10091
|
+
const rectHeightDiff = verticalMargin
|
|
10092
|
+
let rectWidthDiff = verticalMargin / 4
|
|
10093
|
+
let rectHorizontalOffset = 0
|
|
10094
|
+
let rectVerticalOffset = 0
|
|
10095
|
+
|
|
10096
|
+
if (label.anchor & LabelAnchorFlag.LEFT) {
|
|
10097
|
+
left = 0
|
|
10098
|
+
}
|
|
10099
|
+
|
|
10100
|
+
if (label.anchor & LabelAnchorFlag.RIGHT) {
|
|
10101
|
+
left = this.canvas.width - textWidth
|
|
10102
|
+
rectHorizontalOffset -= verticalMargin / 4
|
|
10103
|
+
}
|
|
10104
|
+
|
|
10105
|
+
if (label.anchor & LabelAnchorFlag.CENTER) {
|
|
10106
|
+
left = (this.canvas.width - textWidth) / 2
|
|
10107
|
+
rectHorizontalOffset -= verticalMargin / 4
|
|
10108
|
+
rectWidthDiff += verticalMargin / 4
|
|
10109
|
+
}
|
|
10110
|
+
|
|
10111
|
+
if (label.anchor & LabelAnchorFlag.TOP) {
|
|
10112
|
+
top = 0
|
|
10113
|
+
}
|
|
10114
|
+
|
|
10115
|
+
if (label.anchor & LabelAnchorFlag.MIDDLE) {
|
|
10116
|
+
top = (this.canvas.height - textHeight - verticalMargin) / 2
|
|
10117
|
+
rectVerticalOffset -= verticalMargin / 4
|
|
10118
|
+
}
|
|
10119
|
+
|
|
10120
|
+
if (label.anchor & LabelAnchorFlag.BOTTOM) {
|
|
10121
|
+
top = this.canvas.height - textHeight - verticalMargin
|
|
10122
|
+
rectVerticalOffset -= verticalMargin / 4
|
|
10123
|
+
}
|
|
10124
|
+
this.drawRect(
|
|
10125
|
+
[left + rectHorizontalOffset, top + rectVerticalOffset, textWidth + rectWidthDiff, textHeight + rectHeightDiff],
|
|
10126
|
+
label.style.backgroundColor
|
|
10127
|
+
)
|
|
10128
|
+
this.draw3DLabel(label, [left, top])
|
|
10129
|
+
}
|
|
10130
|
+
}
|
|
10131
|
+
|
|
9266
10132
|
// not included in public docs
|
|
9267
10133
|
draw3D(
|
|
9268
10134
|
leftTopWidthHeight = [0, 0, 0, 0],
|
|
@@ -9355,6 +10221,7 @@ export class Niivue {
|
|
|
9355
10221
|
this.readyForSync = true
|
|
9356
10222
|
this.sync()
|
|
9357
10223
|
this.draw3DLabels(mvpMatrix, relativeLTWH, true)
|
|
10224
|
+
|
|
9358
10225
|
return posString
|
|
9359
10226
|
}
|
|
9360
10227
|
|
|
@@ -10290,7 +11157,14 @@ export class Niivue {
|
|
|
10290
11157
|
}
|
|
10291
11158
|
}
|
|
10292
11159
|
const isDrawPenDown = isFinite(this.drawPenLocation[0]) && this.opts.drawingEnabled
|
|
11160
|
+
|
|
10293
11161
|
const { volScale } = this.sliceScale()
|
|
11162
|
+
const actualScale = volScale.slice()
|
|
11163
|
+
if (this.opts.multiplanarEqualSize) {
|
|
11164
|
+
volScale[0] = 1
|
|
11165
|
+
volScale[1] = 1
|
|
11166
|
+
volScale[2] = 1
|
|
11167
|
+
}
|
|
10294
11168
|
if (typeof this.opts.multiplanarPadPixels !== 'number') {
|
|
10295
11169
|
log.debug('multiplanarPadPixels must be numeric')
|
|
10296
11170
|
}
|
|
@@ -10335,44 +11209,49 @@ export class Niivue {
|
|
|
10335
11209
|
isDrawGrid = true
|
|
10336
11210
|
}
|
|
10337
11211
|
}
|
|
11212
|
+
let ltwh = ltwh2x2
|
|
10338
11213
|
if (isDrawColumn) {
|
|
10339
|
-
|
|
11214
|
+
ltwh = ltwh1x3
|
|
10340
11215
|
if (isShowRender || (this.opts.multiplanarShowRender === SHOW_RENDER.AUTO && ltwh1x4[4] >= ltwh1x3[4])) {
|
|
10341
11216
|
ltwh = ltwh1x4
|
|
10342
11217
|
} else {
|
|
10343
11218
|
isDraw3D = false
|
|
10344
11219
|
}
|
|
10345
|
-
|
|
10346
|
-
|
|
10347
|
-
|
|
10348
|
-
|
|
11220
|
+
} else if (isDrawRow) {
|
|
11221
|
+
ltwh = ltwh3x1
|
|
11222
|
+
if (isShowRender || (this.opts.multiplanarShowRender === SHOW_RENDER.AUTO && ltwh4x1[4] >= ltwh3x1[4])) {
|
|
11223
|
+
ltwh = ltwh4x1
|
|
11224
|
+
} else {
|
|
11225
|
+
isDraw3D = false
|
|
11226
|
+
}
|
|
11227
|
+
}
|
|
11228
|
+
const sX = volScale[0] * ltwh[4]
|
|
11229
|
+
const sY = volScale[1] * ltwh[4]
|
|
11230
|
+
const sZ = volScale[2] * ltwh[4]
|
|
11231
|
+
const actualX = actualScale[0] * ltwh[4]
|
|
11232
|
+
const actualY = actualScale[1] * ltwh[4]
|
|
11233
|
+
const actualZ = actualScale[2] * ltwh[4]
|
|
11234
|
+
if (isDrawColumn) {
|
|
10349
11235
|
// draw axial
|
|
10350
|
-
this.draw2D([ltwh[0], ltwh[1], sX, sY], 0)
|
|
11236
|
+
this.draw2D([ltwh[0], ltwh[1], sX, sY], 0, NaN, [actualX, actualY])
|
|
10351
11237
|
// draw coronal
|
|
10352
|
-
this.draw2D([ltwh[0], ltwh[1] + sY + pad, sX, sZ], 1)
|
|
11238
|
+
this.draw2D([ltwh[0], ltwh[1] + sY + pad, sX, sZ], 1, NaN, [actualX, actualZ])
|
|
10353
11239
|
// draw sagittal
|
|
10354
|
-
this.draw2D([ltwh[0], ltwh[1] + sY + pad + sZ + pad, sY, sZ], 2)
|
|
11240
|
+
this.draw2D([ltwh[0], ltwh[1] + sY + pad + sZ + pad, sY, sZ], 2, NaN, [actualY, actualZ])
|
|
10355
11241
|
if (isDraw3D) {
|
|
11242
|
+
const sMx = mx * ltwh[4]
|
|
10356
11243
|
this.draw3D([ltwh[0], ltwh[1] + sY + sZ + sZ + pad * 3, sMx, sMx])
|
|
10357
11244
|
}
|
|
10358
11245
|
} else if (isDrawRow) {
|
|
10359
|
-
let ltwh = ltwh3x1
|
|
10360
|
-
if (isShowRender || (this.opts.multiplanarShowRender === SHOW_RENDER.AUTO && ltwh4x1[4] >= ltwh3x1[4])) {
|
|
10361
|
-
ltwh = ltwh4x1
|
|
10362
|
-
} else {
|
|
10363
|
-
isDraw3D = false
|
|
10364
|
-
}
|
|
10365
|
-
const sX = volScale[0] * ltwh[4]
|
|
10366
|
-
const sY = volScale[1] * ltwh[4]
|
|
10367
|
-
const sZ = volScale[2] * ltwh[4]
|
|
10368
11246
|
// draw axial
|
|
10369
|
-
this.draw2D([ltwh[0], ltwh[1], sX, sY], 0)
|
|
11247
|
+
this.draw2D([ltwh[0], ltwh[1], sX, sY], 0, NaN, [actualX, actualY])
|
|
10370
11248
|
// draw coronal
|
|
10371
|
-
this.draw2D([ltwh[0] + sX + pad, ltwh[1], sX, sZ], 1)
|
|
11249
|
+
this.draw2D([ltwh[0] + sX + pad, ltwh[1], sX, sZ], 1, NaN, [actualX, actualZ])
|
|
10372
11250
|
// draw sagittal
|
|
10373
|
-
this.draw2D([ltwh[0] + sX + sX + pad * 2, ltwh[1], sY, sZ], 2)
|
|
11251
|
+
this.draw2D([ltwh[0] + sX + sX + pad * 2, ltwh[1], sY, sZ], 2, NaN, [actualY, actualZ])
|
|
10374
11252
|
if (isDraw3D) {
|
|
10375
|
-
|
|
11253
|
+
const sMx = mx * ltwh[4]
|
|
11254
|
+
this.draw3D([ltwh[0] + sX + sX + sY + pad * 3, ltwh[1], sMx, sMx])
|
|
10376
11255
|
}
|
|
10377
11256
|
} else if (isDrawGrid) {
|
|
10378
11257
|
// did the user turn off 3D render view in multiplanar?
|
|
@@ -10383,16 +11262,12 @@ export class Niivue {
|
|
|
10383
11262
|
if (this.opts.multiplanarShowRender === SHOW_RENDER.AUTO) {
|
|
10384
11263
|
isDraw3D = true
|
|
10385
11264
|
}
|
|
10386
|
-
const ltwh = ltwh2x2
|
|
10387
|
-
const sX = volScale[0] * ltwh[4]
|
|
10388
|
-
const sY = volScale[1] * ltwh[4]
|
|
10389
|
-
const sZ = volScale[2] * ltwh[4]
|
|
10390
11265
|
// draw axial
|
|
10391
|
-
this.draw2D([ltwh[0], ltwh[1] + sZ + pad, sX, sY], 0)
|
|
11266
|
+
this.draw2D([ltwh[0], ltwh[1] + sZ + pad, sX, sY], 0, NaN, [actualX, actualY])
|
|
10392
11267
|
// draw coronal
|
|
10393
|
-
this.draw2D([ltwh[0], ltwh[1], sX, sZ], 1)
|
|
11268
|
+
this.draw2D([ltwh[0], ltwh[1], sX, sZ], 1, NaN, [actualX, actualZ])
|
|
10394
11269
|
// draw sagittal
|
|
10395
|
-
this.draw2D([ltwh[0] + sX + pad, ltwh[1], sY, sZ], 2)
|
|
11270
|
+
this.draw2D([ltwh[0] + sX + pad, ltwh[1], sY, sZ], 2, NaN, [actualY, actualZ])
|
|
10396
11271
|
if (isDraw3D) {
|
|
10397
11272
|
this.draw3D([ltwh[0] + sX + pad, ltwh[1] + sZ + pad, sY, sY])
|
|
10398
11273
|
}
|
|
@@ -10457,12 +11332,44 @@ export class Niivue {
|
|
|
10457
11332
|
width,
|
|
10458
11333
|
height
|
|
10459
11334
|
])
|
|
11335
|
+
return
|
|
11336
|
+
}
|
|
11337
|
+
|
|
11338
|
+
// draw circle at mouse position if clickToSegment is enabled
|
|
11339
|
+
if (this.opts.clickToSegment) {
|
|
11340
|
+
const x = this.mousePos[0]
|
|
11341
|
+
const y = this.mousePos[1]
|
|
11342
|
+
// check if hovering over the 3D render tile
|
|
11343
|
+
if (this.inRenderTile(x, y) >= 0) {
|
|
11344
|
+
// exit early since we do not want to draw the cursor here!
|
|
11345
|
+
return
|
|
11346
|
+
}
|
|
11347
|
+
// determine the tile the mouse is hovering in
|
|
11348
|
+
const tileIdx = this.tileIndex(x, y)
|
|
11349
|
+
// if a valid tile index, draw the circle
|
|
11350
|
+
if (tileIdx > -1) {
|
|
11351
|
+
// get fov in mm for this plane presented in the tile
|
|
11352
|
+
const fovMM = this.screenSlices[tileIdx].fovMM
|
|
11353
|
+
// get the left, top, width, height of the tile in pixels
|
|
11354
|
+
const ltwh = this.screenSlices[tileIdx].leftTopWidthHeight
|
|
11355
|
+
// calculate the pixel to mm scale so we can draw the circle
|
|
11356
|
+
// in pixels (so it is highres) but with the radius specified in mm
|
|
11357
|
+
const pixPerMM = ltwh[2] / fovMM[0]
|
|
11358
|
+
// get the crosshair color, but replace the alpha because we want it to be transparent
|
|
11359
|
+
// no matter what. We want to see the image data underneath the circle.
|
|
11360
|
+
const color = this.opts.crosshairColor
|
|
11361
|
+
const segmentCursorColor = [color[0], color[1], color[2], 0.4]
|
|
11362
|
+
const radius = this.opts.clickToSegmentRadius * pixPerMM
|
|
11363
|
+
this.drawCircle([x - radius, y - radius, radius * 2, radius * 2], segmentCursorColor, 1)
|
|
11364
|
+
}
|
|
10460
11365
|
}
|
|
11366
|
+
|
|
10461
11367
|
const pos = this.frac2mm([this.scene.crosshairPos[0], this.scene.crosshairPos[1], this.scene.crosshairPos[2]])
|
|
10462
11368
|
|
|
10463
11369
|
posString = pos[0].toFixed(2) + '×' + pos[1].toFixed(2) + '×' + pos[2].toFixed(2)
|
|
10464
11370
|
this.readyForSync = true // by the time we get here, all volumes should be loaded and ready to be drawn. We let other niivue instances know that we can now reliably sync draw calls (images are loaded)
|
|
10465
11371
|
this.sync()
|
|
11372
|
+
this.drawAnchoredLabels()
|
|
10466
11373
|
return posString
|
|
10467
11374
|
}
|
|
10468
11375
|
|