@niivue/niivue 0.44.0 → 0.44.2-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/build/index.d.ts +132 -18
- package/build/index.js +1088 -178
- package/build/index.js.map +1 -1
- package/build/index.min.js +1 -1
- package/dist/index.d.ts +132 -18
- package/dist/index.js +26585 -30810
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/niivue.umd.js +111 -32
- package/package.json +18 -17
- package/src/index.html +46 -7
- package/src/index_segment.html +165 -0
- package/src/index_sync.html +289 -0
- package/src/niivue/index.ts +857 -137
- package/src/nvdocument.ts +36 -5
- package/src/nvimage/index.ts +29 -3
- package/src/nvimage/utils.ts +61 -42
- package/src/nvlabel.ts +34 -1
- package/src/nvmesh-loaders.ts +50 -6
- package/src/nvmesh-utilities.ts +139 -0
- package/src/nvmesh.ts +96 -0
- package/src/shader-srcs.ts +84 -3
- package/src/types.ts +13 -0
package/src/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,8 +131,10 @@ 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'
|
|
136
|
+
export { NVMeshUtilities } from '../nvmesh-utilities.js'
|
|
137
|
+
|
|
126
138
|
// same rollup error as above during npm run dev, and during the umd build
|
|
127
139
|
// TODO: at least remove the umd build when AFNI do not need it anymore
|
|
128
140
|
export * from '../types.js'
|
|
@@ -184,6 +196,7 @@ type Descriptive = {
|
|
|
184
196
|
cal_max: number
|
|
185
197
|
robust_min: number
|
|
186
198
|
robust_max: number
|
|
199
|
+
area: number | null
|
|
187
200
|
}
|
|
188
201
|
|
|
189
202
|
type SliceScale = {
|
|
@@ -345,6 +358,7 @@ export class Niivue {
|
|
|
345
358
|
orientCubeShader?: Shader
|
|
346
359
|
orientCubeShaderVAO: WebGLVertexArrayObject | null = null
|
|
347
360
|
rectShader?: Shader
|
|
361
|
+
rectOutlineShader?: Shader
|
|
348
362
|
renderShader?: Shader
|
|
349
363
|
lineShader?: Shader
|
|
350
364
|
line3DShader?: Shader
|
|
@@ -390,7 +404,19 @@ export class Niivue {
|
|
|
390
404
|
extentsMax?: vec3
|
|
391
405
|
// ResizeObserver
|
|
392
406
|
private resizeObserver: ResizeObserver | null = null
|
|
393
|
-
syncOpts: Record<string, unknown> = {}
|
|
407
|
+
// syncOpts: Record<string, unknown> = {}
|
|
408
|
+
syncOpts: SyncOpts = {
|
|
409
|
+
'3d': false, // legacy option
|
|
410
|
+
'2d': false, // legacy option
|
|
411
|
+
zoomPan: false,
|
|
412
|
+
cal_min: false,
|
|
413
|
+
cal_max: false,
|
|
414
|
+
clipPlane: false,
|
|
415
|
+
gamma: false,
|
|
416
|
+
sliceType: false,
|
|
417
|
+
crosshair: false
|
|
418
|
+
}
|
|
419
|
+
|
|
394
420
|
readyForSync = false
|
|
395
421
|
|
|
396
422
|
// UI Data
|
|
@@ -440,7 +466,7 @@ export class Niivue {
|
|
|
440
466
|
|
|
441
467
|
cuboidVertexBuffer?: WebGLBuffer
|
|
442
468
|
|
|
443
|
-
otherNV: Niivue
|
|
469
|
+
otherNV: Niivue[] | null = null // another niivue instance that we wish to sync position with
|
|
444
470
|
volumeObject3D: NiivueObject3D | null = null
|
|
445
471
|
pivot3D = [0, 0, 0] // center for rendering rotation
|
|
446
472
|
furthestFromPivot = 10.0 // most distant point from pivot
|
|
@@ -477,6 +503,10 @@ export class Niivue {
|
|
|
477
503
|
Name: 'Hemispheric',
|
|
478
504
|
Frag: fragMeshHemiShader
|
|
479
505
|
},
|
|
506
|
+
{
|
|
507
|
+
Name: 'Crevice',
|
|
508
|
+
Frag: fragMeshShaderCrevice
|
|
509
|
+
},
|
|
480
510
|
{
|
|
481
511
|
Name: 'Edge',
|
|
482
512
|
Frag: fragMeshEdgeShader
|
|
@@ -905,7 +935,11 @@ export class Niivue {
|
|
|
905
935
|
* @deprecated use broadcastTo instead
|
|
906
936
|
* @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
|
|
907
937
|
*/
|
|
908
|
-
syncWith(otherNV: Niivue, syncOpts = { '2d': true, '3d': true }): void {
|
|
938
|
+
syncWith(otherNV: Niivue | Niivue[], syncOpts = { '2d': true, '3d': true }): void {
|
|
939
|
+
// if otherNV is not an array, make it an array of one
|
|
940
|
+
if (!(otherNV instanceof Array)) {
|
|
941
|
+
otherNV = [otherNV]
|
|
942
|
+
}
|
|
909
943
|
this.otherNV = otherNV
|
|
910
944
|
this.syncOpts = { ...syncOpts }
|
|
911
945
|
}
|
|
@@ -922,10 +956,71 @@ export class Niivue {
|
|
|
922
956
|
* @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
|
|
923
957
|
*/
|
|
924
958
|
broadcastTo(otherNV: Niivue | Niivue[], syncOpts = { '2d': true, '3d': true }): void {
|
|
959
|
+
// if otherNV is a single instance then make it an array of one
|
|
960
|
+
if (!(otherNV instanceof Array)) {
|
|
961
|
+
otherNV = [otherNV]
|
|
962
|
+
}
|
|
925
963
|
this.otherNV = otherNV
|
|
926
964
|
this.syncOpts = syncOpts
|
|
927
965
|
}
|
|
928
966
|
|
|
967
|
+
doSync3d(otherNV: Niivue): void {
|
|
968
|
+
otherNV.scene.renderAzimuth = this.scene.renderAzimuth
|
|
969
|
+
otherNV.scene.renderElevation = this.scene.renderElevation
|
|
970
|
+
otherNV.scene.volScaleMultiplier = this.scene.volScaleMultiplier
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// both crosshair and zoomPan
|
|
974
|
+
doSync2d(otherNV: Niivue): void {
|
|
975
|
+
const thisMM = this.frac2mm(this.scene.crosshairPos)
|
|
976
|
+
otherNV.scene.crosshairPos = otherNV.mm2frac(thisMM)
|
|
977
|
+
otherNV.scene.pan2Dxyzmm = vec4.clone(this.scene.pan2Dxyzmm)
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
doSyncGamma(otherNV: Niivue): void {
|
|
981
|
+
// gamma not dependent on 2d/3d
|
|
982
|
+
const thisGamma = this.scene.gamma
|
|
983
|
+
const otherGamma = otherNV.scene.gamma
|
|
984
|
+
if (thisGamma !== otherGamma) {
|
|
985
|
+
otherNV.setGamma(thisGamma)
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
doSyncZoomPan(otherNV: Niivue): void {
|
|
990
|
+
otherNV.scene.pan2Dxyzmm = vec4.clone(this.scene.pan2Dxyzmm)
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
doSyncCrosshair(otherNV: Niivue): void {
|
|
994
|
+
const thisMM = this.frac2mm(this.scene.crosshairPos)
|
|
995
|
+
otherNV.scene.crosshairPos = otherNV.mm2frac(thisMM)
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
doSyncCalMin(otherNV: Niivue): void {
|
|
999
|
+
// only call updateGLVolume if the cal_min is different
|
|
1000
|
+
// because updateGLVolume is expensive, but required to update the volume
|
|
1001
|
+
if (this.volumes[0].cal_min !== otherNV.volumes[0].cal_min) {
|
|
1002
|
+
otherNV.volumes[0].cal_min = this.volumes[0].cal_min
|
|
1003
|
+
otherNV.updateGLVolume()
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
doSyncCalMax(otherNV: Niivue): void {
|
|
1008
|
+
// only call updateGLVolume if the cal_max is different
|
|
1009
|
+
// because updateGLVolume is expensive, but required to update the volume
|
|
1010
|
+
if (this.volumes[0].cal_max !== otherNV.volumes[0].cal_max) {
|
|
1011
|
+
otherNV.volumes[0].cal_max = this.volumes[0].cal_max
|
|
1012
|
+
otherNV.updateGLVolume()
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
doSyncSliceType(otherNV: Niivue): void {
|
|
1017
|
+
otherNV.setSliceType(this.opts.sliceType)
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
doSyncClipPlane(otherNV: Niivue): void {
|
|
1021
|
+
otherNV.setClipPlane(this.scene.clipPlaneDepthAziElev)
|
|
1022
|
+
}
|
|
1023
|
+
|
|
929
1024
|
/**
|
|
930
1025
|
* Sync the scene controls (orientation, crosshair location, etc.) from one Niivue instance to another. useful for using one canvas to drive another.
|
|
931
1026
|
* @internal
|
|
@@ -939,44 +1034,52 @@ export class Niivue {
|
|
|
939
1034
|
if (!this.gl || !this.otherNV || typeof this.otherNV === 'undefined') {
|
|
940
1035
|
return
|
|
941
1036
|
}
|
|
942
|
-
// if (!this.otherNV.readyForSync || !this.readyForSync) {
|
|
943
|
-
// return;
|
|
944
|
-
// }
|
|
945
1037
|
// canvas must have focus to send messages issue706
|
|
946
1038
|
if (!(this.gl.canvas as HTMLCanvasElement).matches(':focus')) {
|
|
947
1039
|
return
|
|
948
1040
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1041
|
+
for (let i = 0; i < this.otherNV.length; i++) {
|
|
1042
|
+
if (this.otherNV[i] === this) {
|
|
1043
|
+
continue
|
|
1044
|
+
}
|
|
1045
|
+
// gamma
|
|
1046
|
+
if (this.syncOpts.gamma) {
|
|
1047
|
+
this.doSyncGamma(this.otherNV[i])
|
|
1048
|
+
}
|
|
1049
|
+
// crosshair
|
|
1050
|
+
if (this.syncOpts.crosshair) {
|
|
1051
|
+
this.doSyncCrosshair(this.otherNV[i])
|
|
1052
|
+
}
|
|
1053
|
+
// zoomPan
|
|
1054
|
+
if (this.syncOpts.zoomPan) {
|
|
1055
|
+
this.doSyncZoomPan(this.otherNV[i])
|
|
1056
|
+
}
|
|
1057
|
+
// sliceType
|
|
1058
|
+
if (this.syncOpts.sliceType) {
|
|
1059
|
+
this.doSyncSliceType(this.otherNV[i])
|
|
1060
|
+
}
|
|
1061
|
+
// cal_min
|
|
1062
|
+
if (this.syncOpts.cal_min) {
|
|
1063
|
+
this.doSyncCalMin(this.otherNV[i])
|
|
1064
|
+
}
|
|
1065
|
+
// cal_max
|
|
1066
|
+
if (this.syncOpts.cal_max) {
|
|
1067
|
+
this.doSyncCalMax(this.otherNV[i])
|
|
1068
|
+
}
|
|
1069
|
+
// clipPlane
|
|
1070
|
+
if (this.syncOpts.clipPlane) {
|
|
1071
|
+
this.doSyncClipPlane(this.otherNV[i])
|
|
1072
|
+
}
|
|
1073
|
+
// legacy 2d option for multiple properties
|
|
952
1074
|
if (this.syncOpts['2d']) {
|
|
953
|
-
this.
|
|
954
|
-
this.otherNV.scene.pan2Dxyzmm = vec4.clone(this.scene.pan2Dxyzmm)
|
|
1075
|
+
this.doSync2d(this.otherNV[i])
|
|
955
1076
|
}
|
|
1077
|
+
// legacy 3d option for multiple properties
|
|
956
1078
|
if (this.syncOpts['3d']) {
|
|
957
|
-
this.
|
|
958
|
-
this.otherNV.scene.renderElevation = this.scene.renderElevation
|
|
959
|
-
this.otherNV.scene.volScaleMultiplier = this.scene.volScaleMultiplier
|
|
960
|
-
}
|
|
961
|
-
this.otherNV.drawScene()
|
|
962
|
-
this.otherNV.createOnLocationChange()
|
|
963
|
-
} else if (Array.isArray(this.otherNV)) {
|
|
964
|
-
for (let i = 0; i < this.otherNV.length; i++) {
|
|
965
|
-
if (this.otherNV[i] === this) {
|
|
966
|
-
continue
|
|
967
|
-
}
|
|
968
|
-
if (this.syncOpts['2d']) {
|
|
969
|
-
this.otherNV[i].scene.crosshairPos = this.otherNV[i].mm2frac(thisMM)
|
|
970
|
-
this.otherNV[i].scene.pan2Dxyzmm = vec4.clone(this.scene.pan2Dxyzmm)
|
|
971
|
-
}
|
|
972
|
-
if (this.syncOpts['3d']) {
|
|
973
|
-
this.otherNV[i].scene.renderAzimuth = this.scene.renderAzimuth
|
|
974
|
-
this.otherNV[i].scene.renderElevation = this.scene.renderElevation
|
|
975
|
-
this.otherNV[i].scene.volScaleMultiplier = this.scene.volScaleMultiplier
|
|
976
|
-
}
|
|
977
|
-
this.otherNV[i].drawScene()
|
|
978
|
-
this.otherNV[i].createOnLocationChange()
|
|
1079
|
+
this.doSync3d(this.otherNV[i])
|
|
979
1080
|
}
|
|
1081
|
+
this.otherNV[i].drawScene()
|
|
1082
|
+
this.otherNV[i].createOnLocationChange()
|
|
980
1083
|
}
|
|
981
1084
|
}
|
|
982
1085
|
|
|
@@ -1097,6 +1200,11 @@ export class Niivue {
|
|
|
1097
1200
|
const [x, y] = [pos.x * this.uiData.dpr!, pos.y * this.uiData.dpr!]
|
|
1098
1201
|
const label = this.getLabelAtPoint([x, y])
|
|
1099
1202
|
if (label) {
|
|
1203
|
+
// check for user defined onclick handler
|
|
1204
|
+
if (label.onClick) {
|
|
1205
|
+
label.onClick(label)
|
|
1206
|
+
return
|
|
1207
|
+
}
|
|
1100
1208
|
// find associated mesh
|
|
1101
1209
|
for (const mesh of this.meshes) {
|
|
1102
1210
|
if (mesh.type !== MeshType.CONNECTOME) {
|
|
@@ -1273,7 +1381,6 @@ export class Niivue {
|
|
|
1273
1381
|
const mmLength = vec3.len(v)
|
|
1274
1382
|
const voxStart = this.frac2vox(fracStart)
|
|
1275
1383
|
const voxEnd = this.frac2vox(fracEnd)
|
|
1276
|
-
|
|
1277
1384
|
this.onDragRelease({
|
|
1278
1385
|
fracStart,
|
|
1279
1386
|
fracEnd,
|
|
@@ -1326,6 +1433,11 @@ export class Niivue {
|
|
|
1326
1433
|
const fracStart = this.canvasPos2frac([this.uiData.dragStart[0], this.uiData.dragStart[1]])
|
|
1327
1434
|
const fracEnd = this.canvasPos2frac([this.uiData.dragEnd[0], this.uiData.dragEnd[1]])
|
|
1328
1435
|
this.generateMouseUpCallback(fracStart, fracEnd)
|
|
1436
|
+
// if roiSelection drag mode
|
|
1437
|
+
if (this.opts.dragMode === DRAG_MODE.roiSelection) {
|
|
1438
|
+
// do not call drawScene so that the selection box remains visible
|
|
1439
|
+
return
|
|
1440
|
+
}
|
|
1329
1441
|
if (this.opts.dragMode !== DRAG_MODE.contrast) {
|
|
1330
1442
|
return
|
|
1331
1443
|
}
|
|
@@ -1445,6 +1557,16 @@ export class Niivue {
|
|
|
1445
1557
|
this.drawScene()
|
|
1446
1558
|
this.uiData.prevX = this.uiData.currX
|
|
1447
1559
|
this.uiData.prevY = this.uiData.currY
|
|
1560
|
+
} else if (!this.uiData.mousedown && this.opts.clickToSegment) {
|
|
1561
|
+
const pos = this.getNoPaddingNoBorderCanvasRelativeMousePosition(e, this.gl.canvas)
|
|
1562
|
+
// ignore if mouse moves outside of tile of initial click
|
|
1563
|
+
if (!pos) {
|
|
1564
|
+
return
|
|
1565
|
+
}
|
|
1566
|
+
const x = pos.x * this.uiData.dpr!
|
|
1567
|
+
const y = pos.y * this.uiData.dpr!
|
|
1568
|
+
this.mousePos = [x, y]
|
|
1569
|
+
this.drawScene()
|
|
1448
1570
|
}
|
|
1449
1571
|
}
|
|
1450
1572
|
|
|
@@ -2049,7 +2171,7 @@ export class Niivue {
|
|
|
2049
2171
|
this.addVolume(volume)
|
|
2050
2172
|
// set drawing enabled to make sure
|
|
2051
2173
|
// the new drawing bitmap matches the background volume dims
|
|
2052
|
-
this.setDrawingEnabled(true)
|
|
2174
|
+
// this.setDrawingEnabled(true)
|
|
2053
2175
|
}
|
|
2054
2176
|
})
|
|
2055
2177
|
.catch((e) => {
|
|
@@ -2807,6 +2929,25 @@ export class Niivue {
|
|
|
2807
2929
|
return this.meshes[idx].indexNearestXYZmm(Xmm, Ymm, Zmm)
|
|
2808
2930
|
}
|
|
2809
2931
|
|
|
2932
|
+
/**
|
|
2933
|
+
* reduce complexity of FreeSurfer mesh
|
|
2934
|
+
* @param mesh - identity of mesh to change
|
|
2935
|
+
* @param order - decimation order 0..6
|
|
2936
|
+
* @example niivue.decimateHierarchicalMesh(niivue.meshes[0].id, 4)
|
|
2937
|
+
* @returns boolean false if mesh is not hierarchical or of lower order
|
|
2938
|
+
* @see {@link https://niivue.github.io/niivue/features/meshes.html | live demo usage}
|
|
2939
|
+
*/
|
|
2940
|
+
decimateHierarchicalMesh(mesh: number, order: number = 3): boolean {
|
|
2941
|
+
const idx = this.getMeshIndexByID(mesh)
|
|
2942
|
+
if (idx < 0) {
|
|
2943
|
+
log.warn('reverseFaces() id not loaded', mesh)
|
|
2944
|
+
return
|
|
2945
|
+
}
|
|
2946
|
+
const ret = this.meshes[idx].decimateHierarchicalMesh(this.gl, order)
|
|
2947
|
+
this.updateGLVolume()
|
|
2948
|
+
return ret
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2810
2951
|
/**
|
|
2811
2952
|
* reverse triangle winding of mesh (swap front and back faces)
|
|
2812
2953
|
* @param id - identity of mesh to change
|
|
@@ -3366,6 +3507,9 @@ export class Niivue {
|
|
|
3366
3507
|
this.renderShader!.use(this.gl)
|
|
3367
3508
|
this.gl.uniform3fv(this.renderShader!.uniforms.clipLo!, this.opts.clipVolumeLow)
|
|
3368
3509
|
this.gl.uniform3fv(this.renderShader!.uniforms.clipHi!, this.opts.clipVolumeHigh)
|
|
3510
|
+
this.pickingImageShader!.use(this.gl)
|
|
3511
|
+
this.gl.uniform3fv(this.pickingImageShader!.uniforms.clipLo!, this.opts.clipVolumeLow)
|
|
3512
|
+
this.gl.uniform3fv(this.pickingImageShader!.uniforms.clipHi!, this.opts.clipVolumeHigh)
|
|
3369
3513
|
this.drawScene()
|
|
3370
3514
|
}
|
|
3371
3515
|
|
|
@@ -4169,6 +4313,29 @@ export class Niivue {
|
|
|
4169
4313
|
y = Math.min(Math.max(y, 0), dy - 1)
|
|
4170
4314
|
z = Math.min(Math.max(z, 0), dz - 1)
|
|
4171
4315
|
this.drawBitmap![x + y * dx + z * dx * dy] = penValue
|
|
4316
|
+
// get tile index for voxel
|
|
4317
|
+
const isAx = this.drawPenAxCorSag === 0
|
|
4318
|
+
const isCor = this.drawPenAxCorSag === 1
|
|
4319
|
+
const isSag = this.drawPenAxCorSag === 2
|
|
4320
|
+
// since the pen is only drawing in one 2D plane,
|
|
4321
|
+
// only draw the neighbors (based on penSize) in that plane.
|
|
4322
|
+
// if penSize is 1, only draw the voxel itself.
|
|
4323
|
+
// if penSize is even (2, 4, 6, etc.), then the extra voxel will be drawn in the positive direction.
|
|
4324
|
+
// if penSize is odd (3, 5, 7, etc.), then the the pen will be centered on the voxel.
|
|
4325
|
+
if (this.opts.penSize > 1) {
|
|
4326
|
+
const halfPenSize = Math.floor(this.opts.penSize / 2)
|
|
4327
|
+
for (let i = -halfPenSize; i <= halfPenSize; i++) {
|
|
4328
|
+
for (let j = -halfPenSize; j <= halfPenSize; j++) {
|
|
4329
|
+
if (isAx) {
|
|
4330
|
+
this.drawBitmap![x + i + (y + j) * dx + z * dx * dy] = penValue
|
|
4331
|
+
} else if (isCor) {
|
|
4332
|
+
this.drawBitmap![x + i + y * dx + (z + j) * dx * dy] = penValue
|
|
4333
|
+
} else if (isSag) {
|
|
4334
|
+
this.drawBitmap![x + (y + j) * dx + (z + i) * dx * dy] = penValue
|
|
4335
|
+
}
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4172
4339
|
}
|
|
4173
4340
|
|
|
4174
4341
|
// not included in public docs
|
|
@@ -4255,6 +4422,140 @@ export class Niivue {
|
|
|
4255
4422
|
}
|
|
4256
4423
|
}
|
|
4257
4424
|
|
|
4425
|
+
/**
|
|
4426
|
+
* Performs a 1-voxel binary dilation on a connected cluster within the drawing mask using the drawFloodFillCore function.
|
|
4427
|
+
*
|
|
4428
|
+
* @param seedXYZ - voxel index of the seed voxel in the mask array.
|
|
4429
|
+
* @param neighbors - Number of neighbors to consider for connectivity and dilation (6, 18, or 26).
|
|
4430
|
+
*/
|
|
4431
|
+
drawingBinaryDilationWithSeed(
|
|
4432
|
+
seedXYZ: number[], // seed voxel x,y,z
|
|
4433
|
+
neighbors: 6 | 18 | 26 = 6
|
|
4434
|
+
): void {
|
|
4435
|
+
try {
|
|
4436
|
+
const mask = this.drawBitmap
|
|
4437
|
+
const xDim = this.back.dims[1]
|
|
4438
|
+
const yDim = this.back.dims[2]
|
|
4439
|
+
const zDim = this.back.dims[3]
|
|
4440
|
+
const nx = xDim
|
|
4441
|
+
const nxy = xDim * yDim
|
|
4442
|
+
const totalVoxels = nxy * zDim
|
|
4443
|
+
function xyz2vx(pt: number[]): number {
|
|
4444
|
+
return pt[0] + pt[1] * nx + pt[2] * nxy
|
|
4445
|
+
}
|
|
4446
|
+
|
|
4447
|
+
const seedIndex = xyz2vx(seedXYZ)
|
|
4448
|
+
|
|
4449
|
+
// check that the seed index is within bounds
|
|
4450
|
+
if (seedIndex < 0 || seedIndex >= totalVoxels) {
|
|
4451
|
+
throw new Error('Seed index is out of bounds.')
|
|
4452
|
+
}
|
|
4453
|
+
|
|
4454
|
+
// get value of the seed voxel
|
|
4455
|
+
const seedValue = mask[seedIndex]
|
|
4456
|
+
|
|
4457
|
+
// check that the seed voxel is filled
|
|
4458
|
+
if (seedValue === 0) {
|
|
4459
|
+
throw new Error('Seed voxel is not part of a filled cluster.')
|
|
4460
|
+
}
|
|
4461
|
+
|
|
4462
|
+
// create a copy of the mask to work on
|
|
4463
|
+
const img = mask.slice()
|
|
4464
|
+
// binarise the img since there could be multiple colors in the mask
|
|
4465
|
+
for (let i = 0; i < totalVoxels; i++) {
|
|
4466
|
+
img[i] = img[i] === seedValue ? 1 : 0
|
|
4467
|
+
}
|
|
4468
|
+
|
|
4469
|
+
// use drawFloodFillCore to identify the connected cluster starting from seedIndex
|
|
4470
|
+
this.drawFloodFillCore(img, seedIndex, neighbors)
|
|
4471
|
+
|
|
4472
|
+
// now, img has the cluster marked with value 2
|
|
4473
|
+
// create an output mask for dilation
|
|
4474
|
+
const outputMask = mask.slice() // Clone the original mask
|
|
4475
|
+
|
|
4476
|
+
// precompute neighbor offsets based on connectivity
|
|
4477
|
+
const neighborOffsets: number[] = []
|
|
4478
|
+
|
|
4479
|
+
// offsets for 6-connectivity (face neighbors)
|
|
4480
|
+
const offsets6 = [-nxy, nxy, -xDim, xDim, -1, 1]
|
|
4481
|
+
|
|
4482
|
+
neighborOffsets.push(...offsets6)
|
|
4483
|
+
|
|
4484
|
+
if (neighbors > 6) {
|
|
4485
|
+
// offsets for 18-connectivity (edge neighbors)
|
|
4486
|
+
neighborOffsets.push(
|
|
4487
|
+
-xDim - 1,
|
|
4488
|
+
-xDim + 1,
|
|
4489
|
+
xDim - 1,
|
|
4490
|
+
xDim + 1,
|
|
4491
|
+
-nxy - xDim,
|
|
4492
|
+
-nxy + xDim,
|
|
4493
|
+
-nxy - 1,
|
|
4494
|
+
-nxy + 1,
|
|
4495
|
+
nxy - xDim,
|
|
4496
|
+
nxy + xDim,
|
|
4497
|
+
nxy - 1,
|
|
4498
|
+
nxy + 1
|
|
4499
|
+
)
|
|
4500
|
+
}
|
|
4501
|
+
|
|
4502
|
+
if (neighbors > 18) {
|
|
4503
|
+
// offsets for 26-connectivity (corner neighbors)
|
|
4504
|
+
neighborOffsets.push(
|
|
4505
|
+
-nxy - xDim - 1,
|
|
4506
|
+
-nxy - xDim + 1,
|
|
4507
|
+
-nxy + xDim - 1,
|
|
4508
|
+
-nxy + xDim + 1,
|
|
4509
|
+
nxy - xDim - 1,
|
|
4510
|
+
nxy - xDim + 1,
|
|
4511
|
+
nxy + xDim - 1,
|
|
4512
|
+
nxy + xDim + 1
|
|
4513
|
+
)
|
|
4514
|
+
}
|
|
4515
|
+
|
|
4516
|
+
// iterate over the cluster voxels (value 2 in img) to perform dilation
|
|
4517
|
+
for (let idx = 0; idx < totalVoxels; idx++) {
|
|
4518
|
+
if (img[idx] === 2) {
|
|
4519
|
+
const x = idx % xDim
|
|
4520
|
+
const y = Math.floor((idx % nxy) / xDim)
|
|
4521
|
+
const z = Math.floor(idx / nxy)
|
|
4522
|
+
|
|
4523
|
+
for (const offset of neighborOffsets) {
|
|
4524
|
+
const neighborIdx = idx + offset
|
|
4525
|
+
|
|
4526
|
+
// skip if neighbor index is out of bounds
|
|
4527
|
+
if (neighborIdx < 0 || neighborIdx >= totalVoxels) {
|
|
4528
|
+
continue
|
|
4529
|
+
}
|
|
4530
|
+
|
|
4531
|
+
// calculate neighbor coordinates
|
|
4532
|
+
const nx = neighborIdx % xDim
|
|
4533
|
+
const ny = Math.floor((neighborIdx % nxy) / xDim)
|
|
4534
|
+
const nz = Math.floor(neighborIdx / nxy)
|
|
4535
|
+
|
|
4536
|
+
// ensure neighbor is adjacent (prevent wrapping around edges)
|
|
4537
|
+
if (Math.abs(nx - x) > 1 || Math.abs(ny - y) > 1 || Math.abs(nz - z) > 1) {
|
|
4538
|
+
continue
|
|
4539
|
+
}
|
|
4540
|
+
|
|
4541
|
+
// if the neighbor voxel is empty in the original mask, fill it in the output mask
|
|
4542
|
+
if (mask[neighborIdx] === 0) {
|
|
4543
|
+
outputMask[neighborIdx] = seedValue
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
// set the output as the new drawing bitmap
|
|
4549
|
+
this.drawBitmap = outputMask
|
|
4550
|
+
// update the undo stack
|
|
4551
|
+
this.drawAddUndoBitmap()
|
|
4552
|
+
// refresh the drawing (copy from cpu to gpu) and show immediately
|
|
4553
|
+
this.refreshDrawing(true)
|
|
4554
|
+
} catch (error) {
|
|
4555
|
+
log.error('Error in drawingBinaryDilationWithSeed:', error)
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
|
|
4258
4559
|
// a voxel can be defined as having 6, 18 or 26 neighbors:
|
|
4259
4560
|
// 6: neighbors share faces (distance=1)
|
|
4260
4561
|
// 18: neighbors share faces (distance=1) or edges (1.4)
|
|
@@ -4357,7 +4658,9 @@ export class Niivue {
|
|
|
4357
4658
|
growSelectedCluster = 0, // if non-zero, growth based on background intensity POSITIVE_INFINITY for selected or bright, NEGATIVE_INFINITY for selected or darker
|
|
4358
4659
|
forceMin = NaN,
|
|
4359
4660
|
forceMax = NaN,
|
|
4360
|
-
neighbors = 6
|
|
4661
|
+
neighbors = 6,
|
|
4662
|
+
// option for only flood filling within max distance from seed voxel
|
|
4663
|
+
maxDistanceMM = Number.POSITIVE_INFINITY
|
|
4361
4664
|
): void {
|
|
4362
4665
|
if (!this.drawBitmap) {
|
|
4363
4666
|
throw new Error('drawBitmap undefined')
|
|
@@ -4383,10 +4686,29 @@ export class Niivue {
|
|
|
4383
4686
|
if (img.length !== nxy * dims[2]) {
|
|
4384
4687
|
return
|
|
4385
4688
|
}
|
|
4689
|
+
function vx2xyz(vx: number): number[] {
|
|
4690
|
+
// provided address in 1D array, return XYZ coordinate
|
|
4691
|
+
const Z = Math.floor(vx / nxy) // slice
|
|
4692
|
+
const Y = Math.floor((vx - Z * nxy) / nx) // column
|
|
4693
|
+
const X = Math.floor(vx % nx)
|
|
4694
|
+
return [X, Y, Z]
|
|
4695
|
+
}
|
|
4386
4696
|
function xyz2vx(pt: number[]): number {
|
|
4387
4697
|
// provided an XYZ 3D point, provide address in 1D array
|
|
4388
4698
|
return pt[0] + pt[1] * nx + pt[2] * nxy
|
|
4389
4699
|
}
|
|
4700
|
+
const vx2mm = (xyz: number[]): vec3 => {
|
|
4701
|
+
return this.vox2mm(xyz, this.back.matRAS)
|
|
4702
|
+
}
|
|
4703
|
+
// store seed vox as mm coordinates
|
|
4704
|
+
const seedMM = vx2mm(seedXYZ)
|
|
4705
|
+
const maxDistanceMM2 = maxDistanceMM ** 2
|
|
4706
|
+
// function to check if new point to be checked is less than maxDistanceMM
|
|
4707
|
+
function isWithinDistance(vx: number): boolean {
|
|
4708
|
+
const xyzMM = vx2mm(vx2xyz(vx))
|
|
4709
|
+
const dist2 = (xyzMM[0] - seedMM[0]) ** 2 + (xyzMM[1] - seedMM[1]) ** 2 + (xyzMM[2] - seedMM[2]) ** 2
|
|
4710
|
+
return dist2 <= maxDistanceMM2
|
|
4711
|
+
}
|
|
4390
4712
|
const seedVx = xyz2vx(seedXYZ)
|
|
4391
4713
|
const seedColor = img[seedVx]
|
|
4392
4714
|
if (seedColor === newColor) {
|
|
@@ -4400,6 +4722,12 @@ export class Niivue {
|
|
|
4400
4722
|
for (let i = 1; i < nxyz; i++) {
|
|
4401
4723
|
img[i] = 0
|
|
4402
4724
|
if (this.drawBitmap[i] === seedColor) {
|
|
4725
|
+
// check if voxel index i is within maxDistanceMM from seed voxel
|
|
4726
|
+
if (!isWithinDistance(i)) {
|
|
4727
|
+
// move to next voxel if not within distance,
|
|
4728
|
+
// no need to check voxel intensity for cluster assignment
|
|
4729
|
+
continue
|
|
4730
|
+
}
|
|
4403
4731
|
img[i] = 1
|
|
4404
4732
|
}
|
|
4405
4733
|
}
|
|
@@ -4431,6 +4759,12 @@ export class Niivue {
|
|
|
4431
4759
|
for (let i = 1; i < nxyz; i++) {
|
|
4432
4760
|
img[i] = 0
|
|
4433
4761
|
if (backImg[i] >= mn && backImg[i] <= mx) {
|
|
4762
|
+
// check if voxel index i is within maxDistanceMM from seed voxel
|
|
4763
|
+
if (!isWithinDistance(i)) {
|
|
4764
|
+
// move to next voxel if not within distance,
|
|
4765
|
+
// no need to check voxel intensity for cluster assignment
|
|
4766
|
+
continue
|
|
4767
|
+
}
|
|
4434
4768
|
img[i] = 1
|
|
4435
4769
|
}
|
|
4436
4770
|
}
|
|
@@ -5139,6 +5473,8 @@ export class Niivue {
|
|
|
5139
5473
|
// rect shader (crosshair): horizontal and vertical lines only
|
|
5140
5474
|
this.rectShader = new Shader(gl, vertRectShader, fragRectShader)
|
|
5141
5475
|
this.rectShader.use(gl)
|
|
5476
|
+
this.rectOutlineShader = new Shader(gl, vertRectShader, fragRectOutlineShader)
|
|
5477
|
+
this.rectOutlineShader.use(gl)
|
|
5142
5478
|
// line shader: diagonal lines
|
|
5143
5479
|
this.lineShader = new Shader(gl, vertLineShader, fragRectShader)
|
|
5144
5480
|
this.lineShader.use(gl)
|
|
@@ -5314,14 +5650,46 @@ export class Niivue {
|
|
|
5314
5650
|
|
|
5315
5651
|
/**
|
|
5316
5652
|
* basic statistics for selected voxel-based image
|
|
5317
|
-
* @param
|
|
5318
|
-
*
|
|
5319
|
-
*
|
|
5320
|
-
*
|
|
5653
|
+
* @param options - an object containing the following properties:
|
|
5654
|
+
* - layer: selects image to describe
|
|
5655
|
+
* - masks: optional binary images to filter voxels
|
|
5656
|
+
* - drawingIsMask: a boolean indicating if the drawing is used as a mask
|
|
5657
|
+
* - roiIsMask: a boolean indicating if the ROI is used as a mask
|
|
5658
|
+
* - startVox: the starting voxel coordinates
|
|
5659
|
+
* - endVox: the ending voxel coordinates
|
|
5660
|
+
* @returns numeric values to describe image or regions of images
|
|
5661
|
+
* @example
|
|
5662
|
+
* niivue.getDescriptives({
|
|
5663
|
+
* layer: 0,
|
|
5664
|
+
* masks: [],
|
|
5665
|
+
* drawingIsMask: true, // drawingIsMask and roiIsMask are mutually exclusive
|
|
5666
|
+
* roiIsMask: false,
|
|
5667
|
+
* startVox: [10, 20, 30], // ignored if roiIsMask is false
|
|
5668
|
+
* endVox: [40, 50, 60] // ignored if roiIsMask is false
|
|
5669
|
+
* });
|
|
5321
5670
|
* @see {@link https://niivue.github.io/niivue/features/draw2.html | live demo usage}
|
|
5322
5671
|
*/
|
|
5323
|
-
getDescriptives(
|
|
5672
|
+
getDescriptives(options: {
|
|
5673
|
+
layer?: number
|
|
5674
|
+
masks?: number[]
|
|
5675
|
+
drawingIsMask?: boolean
|
|
5676
|
+
roiIsMask?: boolean
|
|
5677
|
+
startVox?: number[]
|
|
5678
|
+
endVox?: number[]
|
|
5679
|
+
}): Descriptive {
|
|
5680
|
+
const {
|
|
5681
|
+
layer = 0,
|
|
5682
|
+
masks = [],
|
|
5683
|
+
drawingIsMask = false,
|
|
5684
|
+
roiIsMask = false,
|
|
5685
|
+
startVox = [0, 0, 0],
|
|
5686
|
+
endVox = [0, 0, 0]
|
|
5687
|
+
} = options
|
|
5688
|
+
|
|
5689
|
+
// Rest of the code remains the same
|
|
5690
|
+
let area = null // used if roiIsMask since ROI is in 2D slice
|
|
5324
5691
|
const hdr = this.volumes[layer].hdr!
|
|
5692
|
+
const pixDimsRAS = this.volumes[layer].pixDimsRAS!
|
|
5325
5693
|
let slope = hdr.scl_slope
|
|
5326
5694
|
if (isNaN(slope)) {
|
|
5327
5695
|
slope = 1
|
|
@@ -5360,6 +5728,48 @@ export class Niivue {
|
|
|
5360
5728
|
mask[i] = 0
|
|
5361
5729
|
}
|
|
5362
5730
|
}
|
|
5731
|
+
} else if (masks.length < 1 && roiIsMask) {
|
|
5732
|
+
// fill mask with zeros
|
|
5733
|
+
mask.fill(0)
|
|
5734
|
+
console.log('startVox', startVox)
|
|
5735
|
+
console.log('endVox', endVox)
|
|
5736
|
+
// const startVox = roiInfo.startVox
|
|
5737
|
+
// const endVox = roiInfo.endVox
|
|
5738
|
+
const xrange = this.calculateMinMaxVoxIdx([startVox[0], endVox[0]])
|
|
5739
|
+
const yrange = this.calculateMinMaxVoxIdx([startVox[1], endVox[1]])
|
|
5740
|
+
const zrange = this.calculateMinMaxVoxIdx([startVox[2], endVox[2]])
|
|
5741
|
+
|
|
5742
|
+
// for our constant dimension we add one so that the for loop runs at least once
|
|
5743
|
+
if (startVox[0] - endVox[0] === 0) {
|
|
5744
|
+
xrange[1] = startVox[0] + 1
|
|
5745
|
+
} else if (startVox[1] - endVox[1] === 0) {
|
|
5746
|
+
yrange[1] = startVox[1] + 1
|
|
5747
|
+
} else if (startVox[2] - endVox[2] === 0) {
|
|
5748
|
+
zrange[1] = startVox[2] + 1
|
|
5749
|
+
}
|
|
5750
|
+
|
|
5751
|
+
const xdim = hdr.dims[1]
|
|
5752
|
+
const ydim = hdr.dims[2]
|
|
5753
|
+
for (let z = zrange[0]; z < zrange[1]; z++) {
|
|
5754
|
+
const zi = z * xdim * ydim
|
|
5755
|
+
for (let y = yrange[0]; y < yrange[1]; y++) {
|
|
5756
|
+
const yi = y * xdim
|
|
5757
|
+
for (let x = xrange[0]; x < xrange[1]; x++) {
|
|
5758
|
+
const index = zi + yi + x
|
|
5759
|
+
mask[index] = 1
|
|
5760
|
+
}
|
|
5761
|
+
}
|
|
5762
|
+
}
|
|
5763
|
+
// calculate area 2d area from startVox and endVox coordinates if roiIsMask is true (since mask is created from a 2d slice only)
|
|
5764
|
+
// area is in mm^2. The below calculations work regardless of the orientation of the image since
|
|
5765
|
+
// Niivue converts all images to RAS
|
|
5766
|
+
const xmm = Math.abs(pixDimsRAS[1] * (endVox[0] - startVox[0]))
|
|
5767
|
+
const ymm = Math.abs(pixDimsRAS[2] * (endVox[1] - startVox[1]))
|
|
5768
|
+
const zmm = Math.abs(pixDimsRAS[3] * (endVox[2] - startVox[2]))
|
|
5769
|
+
// one of the above will be zero since roiIsMask==true, so we filter out the non-zero value
|
|
5770
|
+
// and then multiply the other two to get the area
|
|
5771
|
+
const mms = [xmm, ymm, zmm]
|
|
5772
|
+
area = mms.filter((m) => m !== 0).reduce((a, b) => a * b, 1)
|
|
5363
5773
|
}
|
|
5364
5774
|
// Welfords method
|
|
5365
5775
|
// https://www.embeddedrelated.com/showarticle/785.php
|
|
@@ -5420,7 +5830,8 @@ export class Niivue {
|
|
|
5420
5830
|
cal_min: this.volumes[layer].cal_min!,
|
|
5421
5831
|
cal_max: this.volumes[layer].cal_max!,
|
|
5422
5832
|
robust_min: this.volumes[layer].robust_min!,
|
|
5423
|
-
robust_max: this.volumes[layer].robust_max
|
|
5833
|
+
robust_max: this.volumes[layer].robust_max!,
|
|
5834
|
+
area
|
|
5424
5835
|
}
|
|
5425
5836
|
}
|
|
5426
5837
|
|
|
@@ -5947,7 +6358,8 @@ export class Niivue {
|
|
|
5947
6358
|
this.pickingImageShader.use(this.gl)
|
|
5948
6359
|
this.gl.uniform1f(this.pickingImageShader.uniforms.overlays, this.overlays.length)
|
|
5949
6360
|
this.gl.uniform3fv(this.pickingImageShader.uniforms.texVox, vox)
|
|
5950
|
-
|
|
6361
|
+
this.gl.uniform3fv(this.pickingImageShader!.uniforms.clipLo!, this.opts.clipVolumeLow)
|
|
6362
|
+
this.gl.uniform3fv(this.pickingImageShader!.uniforms.clipHi!, this.opts.clipVolumeHigh)
|
|
5951
6363
|
let shader = this.sliceMMShader
|
|
5952
6364
|
if (this.opts.isV1SliceShader) {
|
|
5953
6365
|
shader = this.sliceV1Shader
|
|
@@ -6771,6 +7183,7 @@ export class Niivue {
|
|
|
6771
7183
|
* @see {@link https://niivue.github.io/niivue/features/colormaps.html | live demo usage}
|
|
6772
7184
|
*/
|
|
6773
7185
|
setGamma(gamma = 1.0): void {
|
|
7186
|
+
this.scene.gamma = gamma
|
|
6774
7187
|
cmapper.gamma = gamma
|
|
6775
7188
|
this.updateGLVolume()
|
|
6776
7189
|
}
|
|
@@ -7182,34 +7595,72 @@ export class Niivue {
|
|
|
7182
7595
|
this.scene.crosshairPos = vec3.clone(texFrac)
|
|
7183
7596
|
}
|
|
7184
7597
|
if (this.opts.drawingEnabled) {
|
|
7598
|
+
// drawing done in voxels
|
|
7185
7599
|
const pt = this.frac2vox(this.scene.crosshairPos) as [number, number, number]
|
|
7186
7600
|
// if click-to-segment enabled
|
|
7187
7601
|
if (this.opts.clickToSegment) {
|
|
7188
|
-
|
|
7189
|
-
const
|
|
7602
|
+
// get voxel value of pt
|
|
7603
|
+
const voxelIntensity = this.back.getValue(pt[0], pt[1], pt[2])
|
|
7604
|
+
if (this.opts.clickToSegmentPercent > 0 && this.opts.clickToSegmentPercent <= 1) {
|
|
7605
|
+
this.opts.clickToSegmentIntensityMax = voxelIntensity * (1 + this.opts.clickToSegmentPercent)
|
|
7606
|
+
this.opts.clickToSegmentIntensityMin = voxelIntensity * (1 - this.opts.clickToSegmentPercent)
|
|
7607
|
+
}
|
|
7608
|
+
// if clickToSegmentAutoIntensity, then calculate if we need to flood fill
|
|
7609
|
+
// in a bright or dark region based on the intensity of the clicked voxel,
|
|
7610
|
+
// and where it falls in the range of cal_min and cal_max.
|
|
7611
|
+
// !important! If this option is true, then it will ignore the boolean value of
|
|
7612
|
+
// clickToSegmentBright supplied by the user
|
|
7613
|
+
if (this.opts.clickToSegmentAutoIntensity) {
|
|
7614
|
+
// if voxel intensity is greater than the midpoint of cal_min and cal_max,
|
|
7615
|
+
// then flood fill in a bright region
|
|
7616
|
+
if (voxelIntensity > (this.back.cal_min + this.back.cal_max) * 0.5) {
|
|
7617
|
+
this.opts.clickToSegmentBright = true
|
|
7618
|
+
} else {
|
|
7619
|
+
// else flood fill in a dark region
|
|
7620
|
+
this.opts.clickToSegmentBright = false
|
|
7621
|
+
}
|
|
7622
|
+
}
|
|
7623
|
+
// set brightOrDark now, if clickToSegmentAutoBrightOrDark is false,
|
|
7624
|
+
// then brightOrDark will be set to the value of clickToSegmentBright supplied by the user
|
|
7190
7625
|
const brightOrDark = this.opts.clickToSegmentBright ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY
|
|
7191
|
-
this.drawPenFillPts = []
|
|
7192
7626
|
this.drawPenAxCorSag = axCorSag
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
const
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
this.
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
}
|
|
7627
|
+
if (this.drawPenAxCorSag === SLICE_TYPE.AXIAL) {
|
|
7628
|
+
// get voxel penSize for the current slice, but what if pixDims are not isotropic?
|
|
7629
|
+
// we will use the smallest pixDim for the current slice
|
|
7630
|
+
const pixDims = [this.back.pixDimsRAS[0], this.back.pixDimsRAS[1]]
|
|
7631
|
+
const minPixDim = Math.min(...pixDims)
|
|
7632
|
+
this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim)
|
|
7633
|
+
} else if (this.drawPenAxCorSag === SLICE_TYPE.CORONAL) {
|
|
7634
|
+
const pixDims = [this.back.pixDimsRAS[0], this.back.pixDimsRAS[2]]
|
|
7635
|
+
const minPixDim = Math.min(...pixDims)
|
|
7636
|
+
this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim)
|
|
7637
|
+
} else if (this.drawPenAxCorSag === SLICE_TYPE.SAGITTAL) {
|
|
7638
|
+
const pixDims = [this.back.pixDimsRAS[1], this.back.pixDimsRAS[2]]
|
|
7639
|
+
const minPixDim = Math.min(...pixDims)
|
|
7640
|
+
this.opts.penSize = Math.ceil(this.opts.clickToSegmentRadius / minPixDim)
|
|
7208
7641
|
}
|
|
7642
|
+
// draw the point at the cursor using the penSize to set the
|
|
7643
|
+
// seed for the flood fill
|
|
7644
|
+
// !important! clickToSegment performs two drawing operations,
|
|
7645
|
+
// therefore, if you want to undo the clickToSegment, you need to call undo twice
|
|
7646
|
+
this.drawPt(pt[0], pt[1], pt[2], this.opts.penValue)
|
|
7647
|
+
this.drawFloodFill(
|
|
7648
|
+
[pt[0], pt[1], pt[2]],
|
|
7649
|
+
0,
|
|
7650
|
+
brightOrDark,
|
|
7651
|
+
this.opts.clickToSegmentIntensityMin,
|
|
7652
|
+
this.opts.clickToSegmentIntensityMax,
|
|
7653
|
+
this.opts.floodFillNeighbors,
|
|
7654
|
+
this.opts.clickToSegmentMaxDistanceMM
|
|
7655
|
+
)
|
|
7209
7656
|
this.drawScene()
|
|
7210
7657
|
this.createOnLocationChange(axCorSag)
|
|
7211
7658
|
// get the volume of the segmented region
|
|
7212
|
-
const info = this.getDescriptives(
|
|
7659
|
+
const info = this.getDescriptives({
|
|
7660
|
+
layer: 0,
|
|
7661
|
+
masks: [],
|
|
7662
|
+
drawingIsMask: true
|
|
7663
|
+
})
|
|
7213
7664
|
this.onClickToSegment({ mL: info.volumeML, mm3: info.volumeMM3 })
|
|
7214
7665
|
return
|
|
7215
7666
|
}
|
|
@@ -7461,20 +7912,43 @@ export class Niivue {
|
|
|
7461
7912
|
if (!this.rectShader) {
|
|
7462
7913
|
throw new Error('rectShader undefined')
|
|
7463
7914
|
}
|
|
7464
|
-
this.
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
this.
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7915
|
+
if (!this.opts.selectionBoxIsOutline) {
|
|
7916
|
+
this.rectShader.use(this.gl)
|
|
7917
|
+
this.gl.enable(this.gl.BLEND)
|
|
7918
|
+
this.gl.uniform4fv(this.rectShader.uniforms.lineColor, lineColor)
|
|
7919
|
+
this.gl.uniform2fv(this.rectShader.uniforms.canvasWidthHeight, [this.gl.canvas.width, this.gl.canvas.height])
|
|
7920
|
+
this.gl.uniform4f(
|
|
7921
|
+
this.rectShader.uniforms.leftTopWidthHeight,
|
|
7922
|
+
leftTopWidthHeight[0],
|
|
7923
|
+
leftTopWidthHeight[1],
|
|
7924
|
+
leftTopWidthHeight[2],
|
|
7925
|
+
leftTopWidthHeight[3]
|
|
7926
|
+
)
|
|
7927
|
+
this.gl.bindVertexArray(this.genericVAO)
|
|
7928
|
+
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4)
|
|
7929
|
+
this.gl.bindVertexArray(this.unusedVAO) // switch off to avoid tampering with settings
|
|
7930
|
+
} else {
|
|
7931
|
+
// this.opts.selectionBoxIsOutline == true
|
|
7932
|
+
this.rectOutlineShader.use(this.gl)
|
|
7933
|
+
this.gl.enable(this.gl.BLEND)
|
|
7934
|
+
// set thickness of line
|
|
7935
|
+
this.gl.uniform1f(this.rectOutlineShader.uniforms.thickness, this.opts.selectionBoxLineThickness)
|
|
7936
|
+
this.gl.uniform4fv(this.rectOutlineShader.uniforms.lineColor, lineColor)
|
|
7937
|
+
this.gl.uniform2fv(this.rectOutlineShader.uniforms.canvasWidthHeight, [
|
|
7938
|
+
this.gl.canvas.width,
|
|
7939
|
+
this.gl.canvas.height
|
|
7940
|
+
])
|
|
7941
|
+
this.gl.uniform4f(
|
|
7942
|
+
this.rectOutlineShader.uniforms.leftTopWidthHeight,
|
|
7943
|
+
leftTopWidthHeight[0],
|
|
7944
|
+
leftTopWidthHeight[1],
|
|
7945
|
+
leftTopWidthHeight[2],
|
|
7946
|
+
leftTopWidthHeight[3]
|
|
7947
|
+
)
|
|
7948
|
+
this.gl.bindVertexArray(this.genericVAO)
|
|
7949
|
+
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4)
|
|
7950
|
+
this.gl.bindVertexArray(this.unusedVAO) // switch off to avoid tampering with settings
|
|
7951
|
+
}
|
|
7478
7952
|
}
|
|
7479
7953
|
|
|
7480
7954
|
drawCircle(leftTopWidthHeight: number[], circleColor = this.opts.fontColor, fillPercent = 1.0): void {
|
|
@@ -7526,9 +8000,26 @@ export class Niivue {
|
|
|
7526
8000
|
return labels
|
|
7527
8001
|
}
|
|
7528
8002
|
|
|
8003
|
+
getConnectomeLabels(): NVLabel3D[] {
|
|
8004
|
+
const connectomes = this.meshes.filter((m) => m.type === MeshType.CONNECTOME)
|
|
8005
|
+
const meshNodes = connectomes.flatMap((m) => m.nodes as NVConnectomeNode[])
|
|
8006
|
+
const meshLabels = meshNodes.map((n) => n.label)
|
|
8007
|
+
// filter our undefined labels
|
|
8008
|
+
const definedMeshLabels = meshLabels.filter((l): l is NVLabel3D => l !== undefined)
|
|
8009
|
+
// get all of our non-anchored labels
|
|
8010
|
+
const nonAnchoredLabels = this.document.labels.filter((l) => l.anchor == null || l.anchor === LabelAnchorPoint.NONE)
|
|
8011
|
+
// get the unique set of unanchored labels
|
|
8012
|
+
const nonAnchoredLabelSet = new Set(definedMeshLabels)
|
|
8013
|
+
for (const label of nonAnchoredLabels) {
|
|
8014
|
+
nonAnchoredLabelSet.add(label)
|
|
8015
|
+
}
|
|
8016
|
+
|
|
8017
|
+
return Array.from(nonAnchoredLabelSet)
|
|
8018
|
+
}
|
|
8019
|
+
|
|
7529
8020
|
getBulletMarginWidth(): number {
|
|
7530
8021
|
let bulletMargin = 0
|
|
7531
|
-
const labels = this.
|
|
8022
|
+
const labels = this.getConnectomeLabels()
|
|
7532
8023
|
if (labels.length === 0) {
|
|
7533
8024
|
return 0
|
|
7534
8025
|
}
|
|
@@ -7553,7 +8044,7 @@ export class Niivue {
|
|
|
7553
8044
|
}
|
|
7554
8045
|
|
|
7555
8046
|
getLegendPanelWidth(): number {
|
|
7556
|
-
const labels = this.
|
|
8047
|
+
const labels = this.getConnectomeLabels()
|
|
7557
8048
|
if (!this.opts.showLegend || labels.length === 0) {
|
|
7558
8049
|
return 0
|
|
7559
8050
|
}
|
|
@@ -7584,7 +8075,7 @@ export class Niivue {
|
|
|
7584
8075
|
}
|
|
7585
8076
|
|
|
7586
8077
|
getLegendPanelHeight(): number {
|
|
7587
|
-
const labels = this.
|
|
8078
|
+
const labels = this.getConnectomeLabels()
|
|
7588
8079
|
let height = 0
|
|
7589
8080
|
const scale = 1.0 // we may want to make this adjustable in the future
|
|
7590
8081
|
const verticalMargin = this.opts.textHeight * this.gl.canvas.height * scale
|
|
@@ -8099,7 +8590,11 @@ export class Niivue {
|
|
|
8099
8590
|
|
|
8100
8591
|
// not included in public docs
|
|
8101
8592
|
// show text labels for L/R, A/P, I/S dimensions
|
|
8102
|
-
drawSliceOrientationText(
|
|
8593
|
+
drawSliceOrientationText(
|
|
8594
|
+
leftTopWidthHeight: number[],
|
|
8595
|
+
axCorSag: SLICE_TYPE,
|
|
8596
|
+
padLeftTop: number[] = [NaN, NaN]
|
|
8597
|
+
): void {
|
|
8103
8598
|
this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height)
|
|
8104
8599
|
let topText = 'S'
|
|
8105
8600
|
if (axCorSag === SLICE_TYPE.AXIAL) {
|
|
@@ -8113,9 +8608,32 @@ export class Niivue {
|
|
|
8113
8608
|
this.drawTextRightBelow([leftTopWidthHeight[0], leftTopWidthHeight[1]], leftText + topText)
|
|
8114
8609
|
return
|
|
8115
8610
|
}
|
|
8116
|
-
|
|
8117
|
-
|
|
8118
|
-
|
|
8611
|
+
let drawBelow = true
|
|
8612
|
+
let drawRight = true
|
|
8613
|
+
if (!isNaN(padLeftTop[0])) {
|
|
8614
|
+
const ht = this.opts.textHeight * this.gl.canvas.height + 2
|
|
8615
|
+
if (padLeftTop[1] > ht) {
|
|
8616
|
+
this.drawTextBelow(
|
|
8617
|
+
[leftTopWidthHeight[0] + leftTopWidthHeight[2] * 0.5, leftTopWidthHeight[1] + padLeftTop[1] - ht],
|
|
8618
|
+
topText
|
|
8619
|
+
)
|
|
8620
|
+
drawBelow = false
|
|
8621
|
+
}
|
|
8622
|
+
const wid = this.textWidth(ht, leftText) + 2
|
|
8623
|
+
if (padLeftTop[0] > wid) {
|
|
8624
|
+
this.drawTextRight(
|
|
8625
|
+
[leftTopWidthHeight[0] + padLeftTop[0] - wid, leftTopWidthHeight[1] + leftTopWidthHeight[3] * 0.5],
|
|
8626
|
+
leftText
|
|
8627
|
+
)
|
|
8628
|
+
drawRight = false
|
|
8629
|
+
}
|
|
8630
|
+
}
|
|
8631
|
+
if (drawBelow) {
|
|
8632
|
+
this.drawTextBelow([leftTopWidthHeight[0] + leftTopWidthHeight[2] * 0.5, leftTopWidthHeight[1]], topText)
|
|
8633
|
+
}
|
|
8634
|
+
if (drawRight) {
|
|
8635
|
+
this.drawTextRight([leftTopWidthHeight[0], leftTopWidthHeight[1] + leftTopWidthHeight[3] * 0.5], leftText)
|
|
8636
|
+
}
|
|
8119
8637
|
}
|
|
8120
8638
|
|
|
8121
8639
|
// not included in public docs
|
|
@@ -8158,7 +8676,7 @@ export class Niivue {
|
|
|
8158
8676
|
|
|
8159
8677
|
// not included in public docs
|
|
8160
8678
|
// draw 2D tile
|
|
8161
|
-
|
|
8679
|
+
draw2DMain(leftTopWidthHeight: number[], axCorSag: SLICE_TYPE, customMM = NaN): void {
|
|
8162
8680
|
let frac2mmTexture = this.volumes[0].frac2mm!.slice()
|
|
8163
8681
|
let screen = this.screenFieldOfViewExtendedMM(axCorSag)
|
|
8164
8682
|
let mesh2ortho = mat4.create()
|
|
@@ -8187,8 +8705,10 @@ export class Niivue {
|
|
|
8187
8705
|
elevation = isRadiolgical ? -90 : 90
|
|
8188
8706
|
}
|
|
8189
8707
|
const gl = this.gl
|
|
8708
|
+
let isStretchToScreen = false
|
|
8190
8709
|
if (leftTopWidthHeight[2] === 0 || leftTopWidthHeight[3] === 0) {
|
|
8191
8710
|
// only one tile: stretch tile to fill whole screen.
|
|
8711
|
+
isStretchToScreen = true
|
|
8192
8712
|
const pixPerMMw = gl.canvas.width / screen.fovMM[0]
|
|
8193
8713
|
const pixPerMMh = gl.canvas.height / screen.fovMM[1]
|
|
8194
8714
|
const pixPerMMmin = Math.min(pixPerMMw, pixPerMMh)
|
|
@@ -8347,10 +8867,42 @@ export class Niivue {
|
|
|
8347
8867
|
// no crossbars for mosaic view
|
|
8348
8868
|
this.drawCrosshairs3D(false, 0.15, obj.modelViewProjectionMatrix, true, this.opts.isSliceMM)
|
|
8349
8869
|
}
|
|
8350
|
-
|
|
8870
|
+
if (isStretchToScreen) {
|
|
8871
|
+
// issue1065
|
|
8872
|
+
this.drawSliceOrientationText(leftTopWidthHeight, axCorSag)
|
|
8873
|
+
}
|
|
8351
8874
|
this.readyForSync = true
|
|
8352
8875
|
}
|
|
8353
8876
|
|
|
8877
|
+
draw2D(
|
|
8878
|
+
leftTopWidthHeight: number[],
|
|
8879
|
+
axCorSag: SLICE_TYPE,
|
|
8880
|
+
customMM = NaN,
|
|
8881
|
+
imageWidthHeight: number[] = [NaN, NaN]
|
|
8882
|
+
): void {
|
|
8883
|
+
const padLeftTop = [NaN, NaN]
|
|
8884
|
+
if (isNaN(imageWidthHeight[0])) {
|
|
8885
|
+
this.draw2DMain(leftTopWidthHeight, axCorSag, customMM)
|
|
8886
|
+
} else {
|
|
8887
|
+
// inset as padded in tile
|
|
8888
|
+
const ltwh = leftTopWidthHeight.slice()
|
|
8889
|
+
padLeftTop[0] = Math.floor(0.5 * (ltwh[2] - imageWidthHeight[0]))
|
|
8890
|
+
padLeftTop[1] = Math.floor(0.5 * (ltwh[3] - imageWidthHeight[1]))
|
|
8891
|
+
ltwh[0] += padLeftTop[0]
|
|
8892
|
+
ltwh[1] += padLeftTop[1]
|
|
8893
|
+
ltwh[2] = imageWidthHeight[0]
|
|
8894
|
+
ltwh[3] = imageWidthHeight[1]
|
|
8895
|
+
this.draw2DMain(ltwh, axCorSag, customMM)
|
|
8896
|
+
}
|
|
8897
|
+
if (customMM === Infinity || customMM === -Infinity || axCorSag === SLICE_TYPE.RENDER) {
|
|
8898
|
+
return
|
|
8899
|
+
}
|
|
8900
|
+
if (leftTopWidthHeight[2] !== 0 && leftTopWidthHeight[3] !== 0) {
|
|
8901
|
+
// issue1065
|
|
8902
|
+
this.drawSliceOrientationText(leftTopWidthHeight, axCorSag, padLeftTop)
|
|
8903
|
+
}
|
|
8904
|
+
}
|
|
8905
|
+
|
|
8354
8906
|
// not included in public docs
|
|
8355
8907
|
// determine 3D model view projection matrix
|
|
8356
8908
|
calculateMvpMatrix(_unused: unknown, leftTopWidthHeight = [0, 0, 0, 0], azimuth: number, elevation: number): mat4[] {
|
|
@@ -9049,7 +9601,13 @@ export class Niivue {
|
|
|
9049
9601
|
* @param style - label style
|
|
9050
9602
|
* @param point - 3D point on the model
|
|
9051
9603
|
*/
|
|
9052
|
-
addLabel(
|
|
9604
|
+
addLabel(
|
|
9605
|
+
text: string,
|
|
9606
|
+
style: NVLabel3DStyle,
|
|
9607
|
+
points?: number[] | number[][],
|
|
9608
|
+
anchor?: LabelAnchorPoint,
|
|
9609
|
+
onClick?: (label: NVLabel3D) => void
|
|
9610
|
+
): NVLabel3D {
|
|
9053
9611
|
const defaultStyle = {
|
|
9054
9612
|
textColor: this.opts.legendTextColor,
|
|
9055
9613
|
textScale: 1.0,
|
|
@@ -9061,7 +9619,7 @@ export class Niivue {
|
|
|
9061
9619
|
bulletColor: this.opts.legendTextColor
|
|
9062
9620
|
}
|
|
9063
9621
|
const labelStyle = style ? { ...defaultStyle, ...style } : { ...defaultStyle }
|
|
9064
|
-
const label = new NVLabel3D(text, labelStyle, points)
|
|
9622
|
+
const label = new NVLabel3D(text, { ...labelStyle }, points, anchor, onClick)
|
|
9065
9623
|
this.document.labels.push(label)
|
|
9066
9624
|
return label
|
|
9067
9625
|
}
|
|
@@ -9084,6 +9642,75 @@ export class Niivue {
|
|
|
9084
9642
|
}
|
|
9085
9643
|
|
|
9086
9644
|
getLabelAtPoint(screenPoint: [number, number]): NVLabel3D | null {
|
|
9645
|
+
const scale = 1.0
|
|
9646
|
+
const size = this.opts.textHeight * Math.min(this.gl.canvas.height, this.gl.canvas.width) * scale
|
|
9647
|
+
const verticalMargin = this.opts.textHeight * this.gl.canvas.height * scale
|
|
9648
|
+
|
|
9649
|
+
// get all non-connectome labels
|
|
9650
|
+
for (const label of this.document.labels) {
|
|
9651
|
+
if (label.anchor == null || label.anchor === LabelAnchorPoint.NONE) {
|
|
9652
|
+
continue
|
|
9653
|
+
}
|
|
9654
|
+
|
|
9655
|
+
const labelSize = this.opts.textHeight * this.gl.canvas.height * label.style.textScale
|
|
9656
|
+
const textHeight = this.textHeight(labelSize, label.text)
|
|
9657
|
+
const textWidth = this.textWidth(labelSize, label.text)
|
|
9658
|
+
|
|
9659
|
+
if (label.anchor & LabelAnchorFlag.LEFT) {
|
|
9660
|
+
if (screenPoint[0] > textWidth) {
|
|
9661
|
+
continue
|
|
9662
|
+
}
|
|
9663
|
+
}
|
|
9664
|
+
|
|
9665
|
+
if (label.anchor & LabelAnchorFlag.CENTER) {
|
|
9666
|
+
if (screenPoint[0] < (this.gl.canvas.width - textWidth) / 2) {
|
|
9667
|
+
continue
|
|
9668
|
+
}
|
|
9669
|
+
|
|
9670
|
+
if (screenPoint[0] > (this.gl.canvas.width + textWidth) / 2) {
|
|
9671
|
+
continue
|
|
9672
|
+
}
|
|
9673
|
+
}
|
|
9674
|
+
|
|
9675
|
+
if (label.anchor & LabelAnchorFlag.RIGHT) {
|
|
9676
|
+
if (screenPoint[0] < this.gl.canvas.width - textWidth) {
|
|
9677
|
+
continue
|
|
9678
|
+
}
|
|
9679
|
+
}
|
|
9680
|
+
|
|
9681
|
+
if (label.anchor & LabelAnchorFlag.TOP) {
|
|
9682
|
+
if (screenPoint[1] < verticalMargin / 2) {
|
|
9683
|
+
continue
|
|
9684
|
+
}
|
|
9685
|
+
|
|
9686
|
+
if (screenPoint[1] > textHeight + verticalMargin / 2) {
|
|
9687
|
+
continue
|
|
9688
|
+
}
|
|
9689
|
+
}
|
|
9690
|
+
|
|
9691
|
+
if (label.anchor & LabelAnchorFlag.MIDDLE) {
|
|
9692
|
+
if (screenPoint[1] < (this.gl.canvas.height - textHeight - verticalMargin) / 2) {
|
|
9693
|
+
continue
|
|
9694
|
+
}
|
|
9695
|
+
|
|
9696
|
+
if (screenPoint[1] > (this.gl.canvas.height + textHeight - verticalMargin / 2) / 2) {
|
|
9697
|
+
continue
|
|
9698
|
+
}
|
|
9699
|
+
}
|
|
9700
|
+
|
|
9701
|
+
if (label.anchor & LabelAnchorFlag.BOTTOM) {
|
|
9702
|
+
if (screenPoint[1] < this.gl.canvas.height - textHeight - verticalMargin) {
|
|
9703
|
+
continue
|
|
9704
|
+
}
|
|
9705
|
+
|
|
9706
|
+
if (screenPoint[1] > this.gl.canvas.height - verticalMargin / 2) {
|
|
9707
|
+
continue
|
|
9708
|
+
}
|
|
9709
|
+
}
|
|
9710
|
+
|
|
9711
|
+
// label passed all tests
|
|
9712
|
+
return label
|
|
9713
|
+
}
|
|
9087
9714
|
log.debug('screenPoint', screenPoint)
|
|
9088
9715
|
const panelHeight = this.getLegendPanelHeight()
|
|
9089
9716
|
const panelWidth = this.getLegendPanelWidth()
|
|
@@ -9099,10 +9726,7 @@ export class Niivue {
|
|
|
9099
9726
|
return null
|
|
9100
9727
|
}
|
|
9101
9728
|
|
|
9102
|
-
const
|
|
9103
|
-
const size = this.opts.textHeight * Math.min(this.gl.canvas.height, this.gl.canvas.width) * scale
|
|
9104
|
-
|
|
9105
|
-
const labels = this.getAllLabels()
|
|
9729
|
+
const labels = this.getConnectomeLabels()
|
|
9106
9730
|
for (const label of labels) {
|
|
9107
9731
|
const labelSize = this.opts.textHeight * this.gl.canvas.height * label.style.textScale
|
|
9108
9732
|
const textHeight = this.textHeight(labelSize, label.text)
|
|
@@ -9140,11 +9764,11 @@ export class Niivue {
|
|
|
9140
9764
|
draw3DLabel(
|
|
9141
9765
|
label: NVLabel3D,
|
|
9142
9766
|
pos: vec2,
|
|
9143
|
-
mvpMatrix
|
|
9144
|
-
leftTopWidthHeight
|
|
9145
|
-
bulletMargin
|
|
9146
|
-
legendWidth
|
|
9147
|
-
secondPass
|
|
9767
|
+
mvpMatrix?: mat4,
|
|
9768
|
+
leftTopWidthHeight?: number[],
|
|
9769
|
+
bulletMargin?: number,
|
|
9770
|
+
legendWidth?: number,
|
|
9771
|
+
secondPass?: boolean
|
|
9148
9772
|
): void {
|
|
9149
9773
|
const text = label.text
|
|
9150
9774
|
const left = pos[0]
|
|
@@ -9170,17 +9794,19 @@ export class Niivue {
|
|
|
9170
9794
|
|
|
9171
9795
|
let textLeft = left
|
|
9172
9796
|
|
|
9173
|
-
if (
|
|
9174
|
-
|
|
9175
|
-
|
|
9176
|
-
|
|
9797
|
+
if (legendWidth) {
|
|
9798
|
+
if (label.style.textAlignment !== LabelTextAlignment.LEFT) {
|
|
9799
|
+
const textWidth = this.textWidth(label.style.textScale, label.text) * size
|
|
9800
|
+
if (label.style.textAlignment === LabelTextAlignment.RIGHT) {
|
|
9801
|
+
textLeft = left + legendWidth - size * 1.5 - textWidth
|
|
9802
|
+
} else {
|
|
9803
|
+
const remaining = legendWidth - (bulletMargin || size)
|
|
9804
|
+
textLeft += (remaining - textWidth) / 2
|
|
9805
|
+
}
|
|
9177
9806
|
} else {
|
|
9178
|
-
|
|
9179
|
-
textLeft +=
|
|
9807
|
+
// textLeft += size / 2;
|
|
9808
|
+
textLeft += bulletMargin
|
|
9180
9809
|
}
|
|
9181
|
-
} else {
|
|
9182
|
-
// textLeft += size / 2;
|
|
9183
|
-
textLeft += bulletMargin
|
|
9184
9810
|
}
|
|
9185
9811
|
|
|
9186
9812
|
this.drawText([textLeft, top], text, label.style.textScale, label.style.textColor)
|
|
@@ -9188,7 +9814,7 @@ export class Niivue {
|
|
|
9188
9814
|
|
|
9189
9815
|
// not included in public docs
|
|
9190
9816
|
draw3DLabels(mvpMatrix: mat4, leftTopWidthHeight: number[], secondPass = false): void {
|
|
9191
|
-
const labels = this.
|
|
9817
|
+
const labels = this.getConnectomeLabels()
|
|
9192
9818
|
if (!this.opts.showLegend || labels.length === 0) {
|
|
9193
9819
|
return
|
|
9194
9820
|
}
|
|
@@ -9238,6 +9864,59 @@ export class Niivue {
|
|
|
9238
9864
|
}
|
|
9239
9865
|
}
|
|
9240
9866
|
|
|
9867
|
+
drawAnchoredLabels(): void {
|
|
9868
|
+
const size = this.opts.textHeight * Math.min(this.gl.canvas.height, this.gl.canvas.width) * 1.0
|
|
9869
|
+
const anchoredLabels = this.document.labels.filter((l) => l.anchor != null && l.anchor !== LabelAnchorPoint.NONE)
|
|
9870
|
+
for (const label of anchoredLabels) {
|
|
9871
|
+
const text = label.text
|
|
9872
|
+
const textHeight = this.textHeight(label.style.textScale, text) * size
|
|
9873
|
+
const textWidth = this.textWidth(label.style.textScale, text) * size
|
|
9874
|
+
let left: number
|
|
9875
|
+
let top: number
|
|
9876
|
+
|
|
9877
|
+
const scale = 1.0 // we may want to make this adjustable in the future
|
|
9878
|
+
const verticalMargin = this.opts.textHeight * this.gl.canvas.height * scale
|
|
9879
|
+
const rectHeightDiff = verticalMargin
|
|
9880
|
+
let rectWidthDiff = verticalMargin / 4
|
|
9881
|
+
let rectHorizontalOffset = 0
|
|
9882
|
+
let rectVerticalOffset = 0
|
|
9883
|
+
|
|
9884
|
+
if (label.anchor & LabelAnchorFlag.LEFT) {
|
|
9885
|
+
left = 0
|
|
9886
|
+
}
|
|
9887
|
+
|
|
9888
|
+
if (label.anchor & LabelAnchorFlag.RIGHT) {
|
|
9889
|
+
left = this.canvas.width - textWidth
|
|
9890
|
+
rectHorizontalOffset -= verticalMargin / 4
|
|
9891
|
+
}
|
|
9892
|
+
|
|
9893
|
+
if (label.anchor & LabelAnchorFlag.CENTER) {
|
|
9894
|
+
left = (this.canvas.width - textWidth) / 2
|
|
9895
|
+
rectHorizontalOffset -= verticalMargin / 4
|
|
9896
|
+
rectWidthDiff += verticalMargin / 4
|
|
9897
|
+
}
|
|
9898
|
+
|
|
9899
|
+
if (label.anchor & LabelAnchorFlag.TOP) {
|
|
9900
|
+
top = 0
|
|
9901
|
+
}
|
|
9902
|
+
|
|
9903
|
+
if (label.anchor & LabelAnchorFlag.MIDDLE) {
|
|
9904
|
+
top = (this.canvas.height - textHeight - verticalMargin) / 2
|
|
9905
|
+
rectVerticalOffset -= verticalMargin / 4
|
|
9906
|
+
}
|
|
9907
|
+
|
|
9908
|
+
if (label.anchor & LabelAnchorFlag.BOTTOM) {
|
|
9909
|
+
top = this.canvas.height - textHeight - verticalMargin
|
|
9910
|
+
rectVerticalOffset -= verticalMargin / 4
|
|
9911
|
+
}
|
|
9912
|
+
this.drawRect(
|
|
9913
|
+
[left + rectHorizontalOffset, top + rectVerticalOffset, textWidth + rectWidthDiff, textHeight + rectHeightDiff],
|
|
9914
|
+
label.style.backgroundColor
|
|
9915
|
+
)
|
|
9916
|
+
this.draw3DLabel(label, [left, top])
|
|
9917
|
+
}
|
|
9918
|
+
}
|
|
9919
|
+
|
|
9241
9920
|
// not included in public docs
|
|
9242
9921
|
draw3D(
|
|
9243
9922
|
leftTopWidthHeight = [0, 0, 0, 0],
|
|
@@ -9330,6 +10009,7 @@ export class Niivue {
|
|
|
9330
10009
|
this.readyForSync = true
|
|
9331
10010
|
this.sync()
|
|
9332
10011
|
this.draw3DLabels(mvpMatrix, relativeLTWH, true)
|
|
10012
|
+
|
|
9333
10013
|
return posString
|
|
9334
10014
|
}
|
|
9335
10015
|
|
|
@@ -10265,7 +10945,14 @@ export class Niivue {
|
|
|
10265
10945
|
}
|
|
10266
10946
|
}
|
|
10267
10947
|
const isDrawPenDown = isFinite(this.drawPenLocation[0]) && this.opts.drawingEnabled
|
|
10948
|
+
|
|
10268
10949
|
const { volScale } = this.sliceScale()
|
|
10950
|
+
const actualScale = volScale.slice()
|
|
10951
|
+
if (this.opts.multiplanarEqualSize) {
|
|
10952
|
+
volScale[0] = 1
|
|
10953
|
+
volScale[1] = 1
|
|
10954
|
+
volScale[2] = 1
|
|
10955
|
+
}
|
|
10269
10956
|
if (typeof this.opts.multiplanarPadPixels !== 'number') {
|
|
10270
10957
|
log.debug('multiplanarPadPixels must be numeric')
|
|
10271
10958
|
}
|
|
@@ -10310,44 +10997,49 @@ export class Niivue {
|
|
|
10310
10997
|
isDrawGrid = true
|
|
10311
10998
|
}
|
|
10312
10999
|
}
|
|
11000
|
+
let ltwh = ltwh2x2
|
|
10313
11001
|
if (isDrawColumn) {
|
|
10314
|
-
|
|
11002
|
+
ltwh = ltwh1x3
|
|
10315
11003
|
if (isShowRender || (this.opts.multiplanarShowRender === SHOW_RENDER.AUTO && ltwh1x4[4] >= ltwh1x3[4])) {
|
|
10316
11004
|
ltwh = ltwh1x4
|
|
10317
11005
|
} else {
|
|
10318
11006
|
isDraw3D = false
|
|
10319
11007
|
}
|
|
10320
|
-
|
|
10321
|
-
|
|
10322
|
-
|
|
10323
|
-
|
|
11008
|
+
} else if (isDrawRow) {
|
|
11009
|
+
ltwh = ltwh3x1
|
|
11010
|
+
if (isShowRender || (this.opts.multiplanarShowRender === SHOW_RENDER.AUTO && ltwh4x1[4] >= ltwh3x1[4])) {
|
|
11011
|
+
ltwh = ltwh4x1
|
|
11012
|
+
} else {
|
|
11013
|
+
isDraw3D = false
|
|
11014
|
+
}
|
|
11015
|
+
}
|
|
11016
|
+
const sX = volScale[0] * ltwh[4]
|
|
11017
|
+
const sY = volScale[1] * ltwh[4]
|
|
11018
|
+
const sZ = volScale[2] * ltwh[4]
|
|
11019
|
+
const actualX = actualScale[0] * ltwh[4]
|
|
11020
|
+
const actualY = actualScale[1] * ltwh[4]
|
|
11021
|
+
const actualZ = actualScale[2] * ltwh[4]
|
|
11022
|
+
if (isDrawColumn) {
|
|
10324
11023
|
// draw axial
|
|
10325
|
-
this.draw2D([ltwh[0], ltwh[1], sX, sY], 0)
|
|
11024
|
+
this.draw2D([ltwh[0], ltwh[1], sX, sY], 0, NaN, [actualX, actualY])
|
|
10326
11025
|
// draw coronal
|
|
10327
|
-
this.draw2D([ltwh[0], ltwh[1] + sY + pad, sX, sZ], 1)
|
|
11026
|
+
this.draw2D([ltwh[0], ltwh[1] + sY + pad, sX, sZ], 1, NaN, [actualX, actualZ])
|
|
10328
11027
|
// draw sagittal
|
|
10329
|
-
this.draw2D([ltwh[0], ltwh[1] + sY + pad + sZ + pad, sY, sZ], 2)
|
|
11028
|
+
this.draw2D([ltwh[0], ltwh[1] + sY + pad + sZ + pad, sY, sZ], 2, NaN, [actualY, actualZ])
|
|
10330
11029
|
if (isDraw3D) {
|
|
11030
|
+
const sMx = mx * ltwh[4]
|
|
10331
11031
|
this.draw3D([ltwh[0], ltwh[1] + sY + sZ + sZ + pad * 3, sMx, sMx])
|
|
10332
11032
|
}
|
|
10333
11033
|
} else if (isDrawRow) {
|
|
10334
|
-
let ltwh = ltwh3x1
|
|
10335
|
-
if (isShowRender || (this.opts.multiplanarShowRender === SHOW_RENDER.AUTO && ltwh4x1[4] >= ltwh3x1[4])) {
|
|
10336
|
-
ltwh = ltwh4x1
|
|
10337
|
-
} else {
|
|
10338
|
-
isDraw3D = false
|
|
10339
|
-
}
|
|
10340
|
-
const sX = volScale[0] * ltwh[4]
|
|
10341
|
-
const sY = volScale[1] * ltwh[4]
|
|
10342
|
-
const sZ = volScale[2] * ltwh[4]
|
|
10343
11034
|
// draw axial
|
|
10344
|
-
this.draw2D([ltwh[0], ltwh[1], sX, sY], 0)
|
|
11035
|
+
this.draw2D([ltwh[0], ltwh[1], sX, sY], 0, NaN, [actualX, actualY])
|
|
10345
11036
|
// draw coronal
|
|
10346
|
-
this.draw2D([ltwh[0] + sX + pad, ltwh[1], sX, sZ], 1)
|
|
11037
|
+
this.draw2D([ltwh[0] + sX + pad, ltwh[1], sX, sZ], 1, NaN, [actualX, actualZ])
|
|
10347
11038
|
// draw sagittal
|
|
10348
|
-
this.draw2D([ltwh[0] + sX + sX + pad * 2, ltwh[1], sY, sZ], 2)
|
|
11039
|
+
this.draw2D([ltwh[0] + sX + sX + pad * 2, ltwh[1], sY, sZ], 2, NaN, [actualY, actualZ])
|
|
10349
11040
|
if (isDraw3D) {
|
|
10350
|
-
|
|
11041
|
+
const sMx = mx * ltwh[4]
|
|
11042
|
+
this.draw3D([ltwh[0] + sX + sX + sY + pad * 3, ltwh[1], sMx, sMx])
|
|
10351
11043
|
}
|
|
10352
11044
|
} else if (isDrawGrid) {
|
|
10353
11045
|
// did the user turn off 3D render view in multiplanar?
|
|
@@ -10358,16 +11050,12 @@ export class Niivue {
|
|
|
10358
11050
|
if (this.opts.multiplanarShowRender === SHOW_RENDER.AUTO) {
|
|
10359
11051
|
isDraw3D = true
|
|
10360
11052
|
}
|
|
10361
|
-
const ltwh = ltwh2x2
|
|
10362
|
-
const sX = volScale[0] * ltwh[4]
|
|
10363
|
-
const sY = volScale[1] * ltwh[4]
|
|
10364
|
-
const sZ = volScale[2] * ltwh[4]
|
|
10365
11053
|
// draw axial
|
|
10366
|
-
this.draw2D([ltwh[0], ltwh[1] + sZ + pad, sX, sY], 0)
|
|
11054
|
+
this.draw2D([ltwh[0], ltwh[1] + sZ + pad, sX, sY], 0, NaN, [actualX, actualY])
|
|
10367
11055
|
// draw coronal
|
|
10368
|
-
this.draw2D([ltwh[0], ltwh[1], sX, sZ], 1)
|
|
11056
|
+
this.draw2D([ltwh[0], ltwh[1], sX, sZ], 1, NaN, [actualX, actualZ])
|
|
10369
11057
|
// draw sagittal
|
|
10370
|
-
this.draw2D([ltwh[0] + sX + pad, ltwh[1], sY, sZ], 2)
|
|
11058
|
+
this.draw2D([ltwh[0] + sX + pad, ltwh[1], sY, sZ], 2, NaN, [actualY, actualZ])
|
|
10371
11059
|
if (isDraw3D) {
|
|
10372
11060
|
this.draw3D([ltwh[0] + sX + pad, ltwh[1] + sZ + pad, sY, sY])
|
|
10373
11061
|
}
|
|
@@ -10432,12 +11120,44 @@ export class Niivue {
|
|
|
10432
11120
|
width,
|
|
10433
11121
|
height
|
|
10434
11122
|
])
|
|
11123
|
+
return
|
|
11124
|
+
}
|
|
11125
|
+
|
|
11126
|
+
// draw circle at mouse position if clickToSegment is enabled
|
|
11127
|
+
if (this.opts.clickToSegment) {
|
|
11128
|
+
const x = this.mousePos[0]
|
|
11129
|
+
const y = this.mousePos[1]
|
|
11130
|
+
// check if hovering over the 3D render tile
|
|
11131
|
+
if (this.inRenderTile(x, y) >= 0) {
|
|
11132
|
+
// exit early since we do not want to draw the cursor here!
|
|
11133
|
+
return
|
|
11134
|
+
}
|
|
11135
|
+
// determine the tile the mouse is hovering in
|
|
11136
|
+
const tileIdx = this.tileIndex(x, y)
|
|
11137
|
+
// if a valid tile index, draw the circle
|
|
11138
|
+
if (tileIdx > -1) {
|
|
11139
|
+
// get fov in mm for this plane presented in the tile
|
|
11140
|
+
const fovMM = this.screenSlices[tileIdx].fovMM
|
|
11141
|
+
// get the left, top, width, height of the tile in pixels
|
|
11142
|
+
const ltwh = this.screenSlices[tileIdx].leftTopWidthHeight
|
|
11143
|
+
// calculate the pixel to mm scale so we can draw the circle
|
|
11144
|
+
// in pixels (so it is highres) but with the radius specified in mm
|
|
11145
|
+
const pixPerMM = ltwh[2] / fovMM[0]
|
|
11146
|
+
// get the crosshair color, but replace the alpha because we want it to be transparent
|
|
11147
|
+
// no matter what. We want to see the image data underneath the circle.
|
|
11148
|
+
const color = this.opts.crosshairColor
|
|
11149
|
+
const segmentCursorColor = [color[0], color[1], color[2], 0.4]
|
|
11150
|
+
const radius = this.opts.clickToSegmentRadius * pixPerMM
|
|
11151
|
+
this.drawCircle([x - radius, y - radius, radius * 2, radius * 2], segmentCursorColor, 1)
|
|
11152
|
+
}
|
|
10435
11153
|
}
|
|
11154
|
+
|
|
10436
11155
|
const pos = this.frac2mm([this.scene.crosshairPos[0], this.scene.crosshairPos[1], this.scene.crosshairPos[2]])
|
|
10437
11156
|
|
|
10438
11157
|
posString = pos[0].toFixed(2) + '×' + pos[1].toFixed(2) + '×' + pos[2].toFixed(2)
|
|
10439
11158
|
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)
|
|
10440
11159
|
this.sync()
|
|
11160
|
+
this.drawAnchoredLabels()
|
|
10441
11161
|
return posString
|
|
10442
11162
|
}
|
|
10443
11163
|
|