@niivue/niivue 0.44.1 → 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 +98 -17
- package/build/index.js +827 -169
- package/build/index.js.map +1 -1
- package/build/index.min.js +1 -1
- package/dist/index.d.ts +98 -17
- package/dist/index.js +26209 -30685
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/niivue.umd.js +110 -33
- 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 +831 -136
- package/src/nvdocument.ts +36 -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/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 = {
|
|
@@ -347,6 +358,7 @@ export class Niivue {
|
|
|
347
358
|
orientCubeShader?: Shader
|
|
348
359
|
orientCubeShaderVAO: WebGLVertexArrayObject | null = null
|
|
349
360
|
rectShader?: Shader
|
|
361
|
+
rectOutlineShader?: Shader
|
|
350
362
|
renderShader?: Shader
|
|
351
363
|
lineShader?: Shader
|
|
352
364
|
line3DShader?: Shader
|
|
@@ -392,7 +404,19 @@ export class Niivue {
|
|
|
392
404
|
extentsMax?: vec3
|
|
393
405
|
// ResizeObserver
|
|
394
406
|
private resizeObserver: ResizeObserver | null = null
|
|
395
|
-
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
|
+
|
|
396
420
|
readyForSync = false
|
|
397
421
|
|
|
398
422
|
// UI Data
|
|
@@ -442,7 +466,7 @@ export class Niivue {
|
|
|
442
466
|
|
|
443
467
|
cuboidVertexBuffer?: WebGLBuffer
|
|
444
468
|
|
|
445
|
-
otherNV: Niivue
|
|
469
|
+
otherNV: Niivue[] | null = null // another niivue instance that we wish to sync position with
|
|
446
470
|
volumeObject3D: NiivueObject3D | null = null
|
|
447
471
|
pivot3D = [0, 0, 0] // center for rendering rotation
|
|
448
472
|
furthestFromPivot = 10.0 // most distant point from pivot
|
|
@@ -479,6 +503,10 @@ export class Niivue {
|
|
|
479
503
|
Name: 'Hemispheric',
|
|
480
504
|
Frag: fragMeshHemiShader
|
|
481
505
|
},
|
|
506
|
+
{
|
|
507
|
+
Name: 'Crevice',
|
|
508
|
+
Frag: fragMeshShaderCrevice
|
|
509
|
+
},
|
|
482
510
|
{
|
|
483
511
|
Name: 'Edge',
|
|
484
512
|
Frag: fragMeshEdgeShader
|
|
@@ -907,7 +935,11 @@ export class Niivue {
|
|
|
907
935
|
* @deprecated use broadcastTo instead
|
|
908
936
|
* @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
|
|
909
937
|
*/
|
|
910
|
-
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
|
+
}
|
|
911
943
|
this.otherNV = otherNV
|
|
912
944
|
this.syncOpts = { ...syncOpts }
|
|
913
945
|
}
|
|
@@ -924,10 +956,71 @@ export class Niivue {
|
|
|
924
956
|
* @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
|
|
925
957
|
*/
|
|
926
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
|
+
}
|
|
927
963
|
this.otherNV = otherNV
|
|
928
964
|
this.syncOpts = syncOpts
|
|
929
965
|
}
|
|
930
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
|
+
|
|
931
1024
|
/**
|
|
932
1025
|
* Sync the scene controls (orientation, crosshair location, etc.) from one Niivue instance to another. useful for using one canvas to drive another.
|
|
933
1026
|
* @internal
|
|
@@ -941,44 +1034,52 @@ export class Niivue {
|
|
|
941
1034
|
if (!this.gl || !this.otherNV || typeof this.otherNV === 'undefined') {
|
|
942
1035
|
return
|
|
943
1036
|
}
|
|
944
|
-
// if (!this.otherNV.readyForSync || !this.readyForSync) {
|
|
945
|
-
// return;
|
|
946
|
-
// }
|
|
947
1037
|
// canvas must have focus to send messages issue706
|
|
948
1038
|
if (!(this.gl.canvas as HTMLCanvasElement).matches(':focus')) {
|
|
949
1039
|
return
|
|
950
1040
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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
|
|
954
1074
|
if (this.syncOpts['2d']) {
|
|
955
|
-
this.
|
|
956
|
-
this.otherNV.scene.pan2Dxyzmm = vec4.clone(this.scene.pan2Dxyzmm)
|
|
1075
|
+
this.doSync2d(this.otherNV[i])
|
|
957
1076
|
}
|
|
1077
|
+
// legacy 3d option for multiple properties
|
|
958
1078
|
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()
|
|
1079
|
+
this.doSync3d(this.otherNV[i])
|
|
981
1080
|
}
|
|
1081
|
+
this.otherNV[i].drawScene()
|
|
1082
|
+
this.otherNV[i].createOnLocationChange()
|
|
982
1083
|
}
|
|
983
1084
|
}
|
|
984
1085
|
|
|
@@ -1099,6 +1200,11 @@ export class Niivue {
|
|
|
1099
1200
|
const [x, y] = [pos.x * this.uiData.dpr!, pos.y * this.uiData.dpr!]
|
|
1100
1201
|
const label = this.getLabelAtPoint([x, y])
|
|
1101
1202
|
if (label) {
|
|
1203
|
+
// check for user defined onclick handler
|
|
1204
|
+
if (label.onClick) {
|
|
1205
|
+
label.onClick(label)
|
|
1206
|
+
return
|
|
1207
|
+
}
|
|
1102
1208
|
// find associated mesh
|
|
1103
1209
|
for (const mesh of this.meshes) {
|
|
1104
1210
|
if (mesh.type !== MeshType.CONNECTOME) {
|
|
@@ -1275,7 +1381,6 @@ export class Niivue {
|
|
|
1275
1381
|
const mmLength = vec3.len(v)
|
|
1276
1382
|
const voxStart = this.frac2vox(fracStart)
|
|
1277
1383
|
const voxEnd = this.frac2vox(fracEnd)
|
|
1278
|
-
|
|
1279
1384
|
this.onDragRelease({
|
|
1280
1385
|
fracStart,
|
|
1281
1386
|
fracEnd,
|
|
@@ -1328,6 +1433,11 @@ export class Niivue {
|
|
|
1328
1433
|
const fracStart = this.canvasPos2frac([this.uiData.dragStart[0], this.uiData.dragStart[1]])
|
|
1329
1434
|
const fracEnd = this.canvasPos2frac([this.uiData.dragEnd[0], this.uiData.dragEnd[1]])
|
|
1330
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
|
+
}
|
|
1331
1441
|
if (this.opts.dragMode !== DRAG_MODE.contrast) {
|
|
1332
1442
|
return
|
|
1333
1443
|
}
|
|
@@ -1447,6 +1557,16 @@ export class Niivue {
|
|
|
1447
1557
|
this.drawScene()
|
|
1448
1558
|
this.uiData.prevX = this.uiData.currX
|
|
1449
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()
|
|
1450
1570
|
}
|
|
1451
1571
|
}
|
|
1452
1572
|
|
|
@@ -2051,7 +2171,7 @@ export class Niivue {
|
|
|
2051
2171
|
this.addVolume(volume)
|
|
2052
2172
|
// set drawing enabled to make sure
|
|
2053
2173
|
// the new drawing bitmap matches the background volume dims
|
|
2054
|
-
this.setDrawingEnabled(true)
|
|
2174
|
+
// this.setDrawingEnabled(true)
|
|
2055
2175
|
}
|
|
2056
2176
|
})
|
|
2057
2177
|
.catch((e) => {
|
|
@@ -4193,6 +4313,29 @@ export class Niivue {
|
|
|
4193
4313
|
y = Math.min(Math.max(y, 0), dy - 1)
|
|
4194
4314
|
z = Math.min(Math.max(z, 0), dz - 1)
|
|
4195
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
|
+
}
|
|
4196
4339
|
}
|
|
4197
4340
|
|
|
4198
4341
|
// not included in public docs
|
|
@@ -4279,6 +4422,140 @@ export class Niivue {
|
|
|
4279
4422
|
}
|
|
4280
4423
|
}
|
|
4281
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
|
+
|
|
4282
4559
|
// a voxel can be defined as having 6, 18 or 26 neighbors:
|
|
4283
4560
|
// 6: neighbors share faces (distance=1)
|
|
4284
4561
|
// 18: neighbors share faces (distance=1) or edges (1.4)
|
|
@@ -4381,7 +4658,9 @@ export class Niivue {
|
|
|
4381
4658
|
growSelectedCluster = 0, // if non-zero, growth based on background intensity POSITIVE_INFINITY for selected or bright, NEGATIVE_INFINITY for selected or darker
|
|
4382
4659
|
forceMin = NaN,
|
|
4383
4660
|
forceMax = NaN,
|
|
4384
|
-
neighbors = 6
|
|
4661
|
+
neighbors = 6,
|
|
4662
|
+
// option for only flood filling within max distance from seed voxel
|
|
4663
|
+
maxDistanceMM = Number.POSITIVE_INFINITY
|
|
4385
4664
|
): void {
|
|
4386
4665
|
if (!this.drawBitmap) {
|
|
4387
4666
|
throw new Error('drawBitmap undefined')
|
|
@@ -4407,10 +4686,29 @@ export class Niivue {
|
|
|
4407
4686
|
if (img.length !== nxy * dims[2]) {
|
|
4408
4687
|
return
|
|
4409
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
|
+
}
|
|
4410
4696
|
function xyz2vx(pt: number[]): number {
|
|
4411
4697
|
// provided an XYZ 3D point, provide address in 1D array
|
|
4412
4698
|
return pt[0] + pt[1] * nx + pt[2] * nxy
|
|
4413
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
|
+
}
|
|
4414
4712
|
const seedVx = xyz2vx(seedXYZ)
|
|
4415
4713
|
const seedColor = img[seedVx]
|
|
4416
4714
|
if (seedColor === newColor) {
|
|
@@ -4424,6 +4722,12 @@ export class Niivue {
|
|
|
4424
4722
|
for (let i = 1; i < nxyz; i++) {
|
|
4425
4723
|
img[i] = 0
|
|
4426
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
|
+
}
|
|
4427
4731
|
img[i] = 1
|
|
4428
4732
|
}
|
|
4429
4733
|
}
|
|
@@ -4455,6 +4759,12 @@ export class Niivue {
|
|
|
4455
4759
|
for (let i = 1; i < nxyz; i++) {
|
|
4456
4760
|
img[i] = 0
|
|
4457
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
|
+
}
|
|
4458
4768
|
img[i] = 1
|
|
4459
4769
|
}
|
|
4460
4770
|
}
|
|
@@ -5163,6 +5473,8 @@ export class Niivue {
|
|
|
5163
5473
|
// rect shader (crosshair): horizontal and vertical lines only
|
|
5164
5474
|
this.rectShader = new Shader(gl, vertRectShader, fragRectShader)
|
|
5165
5475
|
this.rectShader.use(gl)
|
|
5476
|
+
this.rectOutlineShader = new Shader(gl, vertRectShader, fragRectOutlineShader)
|
|
5477
|
+
this.rectOutlineShader.use(gl)
|
|
5166
5478
|
// line shader: diagonal lines
|
|
5167
5479
|
this.lineShader = new Shader(gl, vertLineShader, fragRectShader)
|
|
5168
5480
|
this.lineShader.use(gl)
|
|
@@ -5338,14 +5650,46 @@ export class Niivue {
|
|
|
5338
5650
|
|
|
5339
5651
|
/**
|
|
5340
5652
|
* basic statistics for selected voxel-based image
|
|
5341
|
-
* @param
|
|
5342
|
-
*
|
|
5343
|
-
*
|
|
5344
|
-
*
|
|
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
|
+
* });
|
|
5345
5670
|
* @see {@link https://niivue.github.io/niivue/features/draw2.html | live demo usage}
|
|
5346
5671
|
*/
|
|
5347
|
-
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
|
|
5348
5691
|
const hdr = this.volumes[layer].hdr!
|
|
5692
|
+
const pixDimsRAS = this.volumes[layer].pixDimsRAS!
|
|
5349
5693
|
let slope = hdr.scl_slope
|
|
5350
5694
|
if (isNaN(slope)) {
|
|
5351
5695
|
slope = 1
|
|
@@ -5384,6 +5728,48 @@ export class Niivue {
|
|
|
5384
5728
|
mask[i] = 0
|
|
5385
5729
|
}
|
|
5386
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)
|
|
5387
5773
|
}
|
|
5388
5774
|
// Welfords method
|
|
5389
5775
|
// https://www.embeddedrelated.com/showarticle/785.php
|
|
@@ -5444,7 +5830,8 @@ export class Niivue {
|
|
|
5444
5830
|
cal_min: this.volumes[layer].cal_min!,
|
|
5445
5831
|
cal_max: this.volumes[layer].cal_max!,
|
|
5446
5832
|
robust_min: this.volumes[layer].robust_min!,
|
|
5447
|
-
robust_max: this.volumes[layer].robust_max
|
|
5833
|
+
robust_max: this.volumes[layer].robust_max!,
|
|
5834
|
+
area
|
|
5448
5835
|
}
|
|
5449
5836
|
}
|
|
5450
5837
|
|
|
@@ -6796,6 +7183,7 @@ export class Niivue {
|
|
|
6796
7183
|
* @see {@link https://niivue.github.io/niivue/features/colormaps.html | live demo usage}
|
|
6797
7184
|
*/
|
|
6798
7185
|
setGamma(gamma = 1.0): void {
|
|
7186
|
+
this.scene.gamma = gamma
|
|
6799
7187
|
cmapper.gamma = gamma
|
|
6800
7188
|
this.updateGLVolume()
|
|
6801
7189
|
}
|
|
@@ -7207,34 +7595,72 @@ export class Niivue {
|
|
|
7207
7595
|
this.scene.crosshairPos = vec3.clone(texFrac)
|
|
7208
7596
|
}
|
|
7209
7597
|
if (this.opts.drawingEnabled) {
|
|
7598
|
+
// drawing done in voxels
|
|
7210
7599
|
const pt = this.frac2vox(this.scene.crosshairPos) as [number, number, number]
|
|
7211
7600
|
// if click-to-segment enabled
|
|
7212
7601
|
if (this.opts.clickToSegment) {
|
|
7213
|
-
|
|
7214
|
-
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
|
|
7215
7625
|
const brightOrDark = this.opts.clickToSegmentBright ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY
|
|
7216
|
-
this.drawPenFillPts = []
|
|
7217
7626
|
this.drawPenAxCorSag = axCorSag
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
const
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
this.
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
}
|
|
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)
|
|
7233
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
|
+
)
|
|
7234
7656
|
this.drawScene()
|
|
7235
7657
|
this.createOnLocationChange(axCorSag)
|
|
7236
7658
|
// get the volume of the segmented region
|
|
7237
|
-
const info = this.getDescriptives(
|
|
7659
|
+
const info = this.getDescriptives({
|
|
7660
|
+
layer: 0,
|
|
7661
|
+
masks: [],
|
|
7662
|
+
drawingIsMask: true
|
|
7663
|
+
})
|
|
7238
7664
|
this.onClickToSegment({ mL: info.volumeML, mm3: info.volumeMM3 })
|
|
7239
7665
|
return
|
|
7240
7666
|
}
|
|
@@ -7486,20 +7912,43 @@ export class Niivue {
|
|
|
7486
7912
|
if (!this.rectShader) {
|
|
7487
7913
|
throw new Error('rectShader undefined')
|
|
7488
7914
|
}
|
|
7489
|
-
this.
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
this.
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
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
|
+
}
|
|
7503
7952
|
}
|
|
7504
7953
|
|
|
7505
7954
|
drawCircle(leftTopWidthHeight: number[], circleColor = this.opts.fontColor, fillPercent = 1.0): void {
|
|
@@ -7551,9 +8000,26 @@ export class Niivue {
|
|
|
7551
8000
|
return labels
|
|
7552
8001
|
}
|
|
7553
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
|
+
|
|
7554
8020
|
getBulletMarginWidth(): number {
|
|
7555
8021
|
let bulletMargin = 0
|
|
7556
|
-
const labels = this.
|
|
8022
|
+
const labels = this.getConnectomeLabels()
|
|
7557
8023
|
if (labels.length === 0) {
|
|
7558
8024
|
return 0
|
|
7559
8025
|
}
|
|
@@ -7578,7 +8044,7 @@ export class Niivue {
|
|
|
7578
8044
|
}
|
|
7579
8045
|
|
|
7580
8046
|
getLegendPanelWidth(): number {
|
|
7581
|
-
const labels = this.
|
|
8047
|
+
const labels = this.getConnectomeLabels()
|
|
7582
8048
|
if (!this.opts.showLegend || labels.length === 0) {
|
|
7583
8049
|
return 0
|
|
7584
8050
|
}
|
|
@@ -7609,7 +8075,7 @@ export class Niivue {
|
|
|
7609
8075
|
}
|
|
7610
8076
|
|
|
7611
8077
|
getLegendPanelHeight(): number {
|
|
7612
|
-
const labels = this.
|
|
8078
|
+
const labels = this.getConnectomeLabels()
|
|
7613
8079
|
let height = 0
|
|
7614
8080
|
const scale = 1.0 // we may want to make this adjustable in the future
|
|
7615
8081
|
const verticalMargin = this.opts.textHeight * this.gl.canvas.height * scale
|
|
@@ -8124,7 +8590,11 @@ export class Niivue {
|
|
|
8124
8590
|
|
|
8125
8591
|
// not included in public docs
|
|
8126
8592
|
// show text labels for L/R, A/P, I/S dimensions
|
|
8127
|
-
drawSliceOrientationText(
|
|
8593
|
+
drawSliceOrientationText(
|
|
8594
|
+
leftTopWidthHeight: number[],
|
|
8595
|
+
axCorSag: SLICE_TYPE,
|
|
8596
|
+
padLeftTop: number[] = [NaN, NaN]
|
|
8597
|
+
): void {
|
|
8128
8598
|
this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height)
|
|
8129
8599
|
let topText = 'S'
|
|
8130
8600
|
if (axCorSag === SLICE_TYPE.AXIAL) {
|
|
@@ -8138,9 +8608,32 @@ export class Niivue {
|
|
|
8138
8608
|
this.drawTextRightBelow([leftTopWidthHeight[0], leftTopWidthHeight[1]], leftText + topText)
|
|
8139
8609
|
return
|
|
8140
8610
|
}
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
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
|
+
}
|
|
8144
8637
|
}
|
|
8145
8638
|
|
|
8146
8639
|
// not included in public docs
|
|
@@ -8183,7 +8676,7 @@ export class Niivue {
|
|
|
8183
8676
|
|
|
8184
8677
|
// not included in public docs
|
|
8185
8678
|
// draw 2D tile
|
|
8186
|
-
|
|
8679
|
+
draw2DMain(leftTopWidthHeight: number[], axCorSag: SLICE_TYPE, customMM = NaN): void {
|
|
8187
8680
|
let frac2mmTexture = this.volumes[0].frac2mm!.slice()
|
|
8188
8681
|
let screen = this.screenFieldOfViewExtendedMM(axCorSag)
|
|
8189
8682
|
let mesh2ortho = mat4.create()
|
|
@@ -8212,8 +8705,10 @@ export class Niivue {
|
|
|
8212
8705
|
elevation = isRadiolgical ? -90 : 90
|
|
8213
8706
|
}
|
|
8214
8707
|
const gl = this.gl
|
|
8708
|
+
let isStretchToScreen = false
|
|
8215
8709
|
if (leftTopWidthHeight[2] === 0 || leftTopWidthHeight[3] === 0) {
|
|
8216
8710
|
// only one tile: stretch tile to fill whole screen.
|
|
8711
|
+
isStretchToScreen = true
|
|
8217
8712
|
const pixPerMMw = gl.canvas.width / screen.fovMM[0]
|
|
8218
8713
|
const pixPerMMh = gl.canvas.height / screen.fovMM[1]
|
|
8219
8714
|
const pixPerMMmin = Math.min(pixPerMMw, pixPerMMh)
|
|
@@ -8372,10 +8867,42 @@ export class Niivue {
|
|
|
8372
8867
|
// no crossbars for mosaic view
|
|
8373
8868
|
this.drawCrosshairs3D(false, 0.15, obj.modelViewProjectionMatrix, true, this.opts.isSliceMM)
|
|
8374
8869
|
}
|
|
8375
|
-
|
|
8870
|
+
if (isStretchToScreen) {
|
|
8871
|
+
// issue1065
|
|
8872
|
+
this.drawSliceOrientationText(leftTopWidthHeight, axCorSag)
|
|
8873
|
+
}
|
|
8376
8874
|
this.readyForSync = true
|
|
8377
8875
|
}
|
|
8378
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
|
+
|
|
8379
8906
|
// not included in public docs
|
|
8380
8907
|
// determine 3D model view projection matrix
|
|
8381
8908
|
calculateMvpMatrix(_unused: unknown, leftTopWidthHeight = [0, 0, 0, 0], azimuth: number, elevation: number): mat4[] {
|
|
@@ -9074,7 +9601,13 @@ export class Niivue {
|
|
|
9074
9601
|
* @param style - label style
|
|
9075
9602
|
* @param point - 3D point on the model
|
|
9076
9603
|
*/
|
|
9077
|
-
addLabel(
|
|
9604
|
+
addLabel(
|
|
9605
|
+
text: string,
|
|
9606
|
+
style: NVLabel3DStyle,
|
|
9607
|
+
points?: number[] | number[][],
|
|
9608
|
+
anchor?: LabelAnchorPoint,
|
|
9609
|
+
onClick?: (label: NVLabel3D) => void
|
|
9610
|
+
): NVLabel3D {
|
|
9078
9611
|
const defaultStyle = {
|
|
9079
9612
|
textColor: this.opts.legendTextColor,
|
|
9080
9613
|
textScale: 1.0,
|
|
@@ -9086,7 +9619,7 @@ export class Niivue {
|
|
|
9086
9619
|
bulletColor: this.opts.legendTextColor
|
|
9087
9620
|
}
|
|
9088
9621
|
const labelStyle = style ? { ...defaultStyle, ...style } : { ...defaultStyle }
|
|
9089
|
-
const label = new NVLabel3D(text, labelStyle, points)
|
|
9622
|
+
const label = new NVLabel3D(text, { ...labelStyle }, points, anchor, onClick)
|
|
9090
9623
|
this.document.labels.push(label)
|
|
9091
9624
|
return label
|
|
9092
9625
|
}
|
|
@@ -9109,6 +9642,75 @@ export class Niivue {
|
|
|
9109
9642
|
}
|
|
9110
9643
|
|
|
9111
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
|
+
}
|
|
9112
9714
|
log.debug('screenPoint', screenPoint)
|
|
9113
9715
|
const panelHeight = this.getLegendPanelHeight()
|
|
9114
9716
|
const panelWidth = this.getLegendPanelWidth()
|
|
@@ -9124,10 +9726,7 @@ export class Niivue {
|
|
|
9124
9726
|
return null
|
|
9125
9727
|
}
|
|
9126
9728
|
|
|
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()
|
|
9729
|
+
const labels = this.getConnectomeLabels()
|
|
9131
9730
|
for (const label of labels) {
|
|
9132
9731
|
const labelSize = this.opts.textHeight * this.gl.canvas.height * label.style.textScale
|
|
9133
9732
|
const textHeight = this.textHeight(labelSize, label.text)
|
|
@@ -9165,11 +9764,11 @@ export class Niivue {
|
|
|
9165
9764
|
draw3DLabel(
|
|
9166
9765
|
label: NVLabel3D,
|
|
9167
9766
|
pos: vec2,
|
|
9168
|
-
mvpMatrix
|
|
9169
|
-
leftTopWidthHeight
|
|
9170
|
-
bulletMargin
|
|
9171
|
-
legendWidth
|
|
9172
|
-
secondPass
|
|
9767
|
+
mvpMatrix?: mat4,
|
|
9768
|
+
leftTopWidthHeight?: number[],
|
|
9769
|
+
bulletMargin?: number,
|
|
9770
|
+
legendWidth?: number,
|
|
9771
|
+
secondPass?: boolean
|
|
9173
9772
|
): void {
|
|
9174
9773
|
const text = label.text
|
|
9175
9774
|
const left = pos[0]
|
|
@@ -9195,17 +9794,19 @@ export class Niivue {
|
|
|
9195
9794
|
|
|
9196
9795
|
let textLeft = left
|
|
9197
9796
|
|
|
9198
|
-
if (
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
|
|
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
|
+
}
|
|
9202
9806
|
} else {
|
|
9203
|
-
|
|
9204
|
-
textLeft +=
|
|
9807
|
+
// textLeft += size / 2;
|
|
9808
|
+
textLeft += bulletMargin
|
|
9205
9809
|
}
|
|
9206
|
-
} else {
|
|
9207
|
-
// textLeft += size / 2;
|
|
9208
|
-
textLeft += bulletMargin
|
|
9209
9810
|
}
|
|
9210
9811
|
|
|
9211
9812
|
this.drawText([textLeft, top], text, label.style.textScale, label.style.textColor)
|
|
@@ -9213,7 +9814,7 @@ export class Niivue {
|
|
|
9213
9814
|
|
|
9214
9815
|
// not included in public docs
|
|
9215
9816
|
draw3DLabels(mvpMatrix: mat4, leftTopWidthHeight: number[], secondPass = false): void {
|
|
9216
|
-
const labels = this.
|
|
9817
|
+
const labels = this.getConnectomeLabels()
|
|
9217
9818
|
if (!this.opts.showLegend || labels.length === 0) {
|
|
9218
9819
|
return
|
|
9219
9820
|
}
|
|
@@ -9263,6 +9864,59 @@ export class Niivue {
|
|
|
9263
9864
|
}
|
|
9264
9865
|
}
|
|
9265
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
|
+
|
|
9266
9920
|
// not included in public docs
|
|
9267
9921
|
draw3D(
|
|
9268
9922
|
leftTopWidthHeight = [0, 0, 0, 0],
|
|
@@ -9355,6 +10009,7 @@ export class Niivue {
|
|
|
9355
10009
|
this.readyForSync = true
|
|
9356
10010
|
this.sync()
|
|
9357
10011
|
this.draw3DLabels(mvpMatrix, relativeLTWH, true)
|
|
10012
|
+
|
|
9358
10013
|
return posString
|
|
9359
10014
|
}
|
|
9360
10015
|
|
|
@@ -10290,7 +10945,14 @@ export class Niivue {
|
|
|
10290
10945
|
}
|
|
10291
10946
|
}
|
|
10292
10947
|
const isDrawPenDown = isFinite(this.drawPenLocation[0]) && this.opts.drawingEnabled
|
|
10948
|
+
|
|
10293
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
|
+
}
|
|
10294
10956
|
if (typeof this.opts.multiplanarPadPixels !== 'number') {
|
|
10295
10957
|
log.debug('multiplanarPadPixels must be numeric')
|
|
10296
10958
|
}
|
|
@@ -10335,44 +10997,49 @@ export class Niivue {
|
|
|
10335
10997
|
isDrawGrid = true
|
|
10336
10998
|
}
|
|
10337
10999
|
}
|
|
11000
|
+
let ltwh = ltwh2x2
|
|
10338
11001
|
if (isDrawColumn) {
|
|
10339
|
-
|
|
11002
|
+
ltwh = ltwh1x3
|
|
10340
11003
|
if (isShowRender || (this.opts.multiplanarShowRender === SHOW_RENDER.AUTO && ltwh1x4[4] >= ltwh1x3[4])) {
|
|
10341
11004
|
ltwh = ltwh1x4
|
|
10342
11005
|
} else {
|
|
10343
11006
|
isDraw3D = false
|
|
10344
11007
|
}
|
|
10345
|
-
|
|
10346
|
-
|
|
10347
|
-
|
|
10348
|
-
|
|
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) {
|
|
10349
11023
|
// draw axial
|
|
10350
|
-
this.draw2D([ltwh[0], ltwh[1], sX, sY], 0)
|
|
11024
|
+
this.draw2D([ltwh[0], ltwh[1], sX, sY], 0, NaN, [actualX, actualY])
|
|
10351
11025
|
// draw coronal
|
|
10352
|
-
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])
|
|
10353
11027
|
// draw sagittal
|
|
10354
|
-
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])
|
|
10355
11029
|
if (isDraw3D) {
|
|
11030
|
+
const sMx = mx * ltwh[4]
|
|
10356
11031
|
this.draw3D([ltwh[0], ltwh[1] + sY + sZ + sZ + pad * 3, sMx, sMx])
|
|
10357
11032
|
}
|
|
10358
11033
|
} 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
11034
|
// draw axial
|
|
10369
|
-
this.draw2D([ltwh[0], ltwh[1], sX, sY], 0)
|
|
11035
|
+
this.draw2D([ltwh[0], ltwh[1], sX, sY], 0, NaN, [actualX, actualY])
|
|
10370
11036
|
// draw coronal
|
|
10371
|
-
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])
|
|
10372
11038
|
// draw sagittal
|
|
10373
|
-
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])
|
|
10374
11040
|
if (isDraw3D) {
|
|
10375
|
-
|
|
11041
|
+
const sMx = mx * ltwh[4]
|
|
11042
|
+
this.draw3D([ltwh[0] + sX + sX + sY + pad * 3, ltwh[1], sMx, sMx])
|
|
10376
11043
|
}
|
|
10377
11044
|
} else if (isDrawGrid) {
|
|
10378
11045
|
// did the user turn off 3D render view in multiplanar?
|
|
@@ -10383,16 +11050,12 @@ export class Niivue {
|
|
|
10383
11050
|
if (this.opts.multiplanarShowRender === SHOW_RENDER.AUTO) {
|
|
10384
11051
|
isDraw3D = true
|
|
10385
11052
|
}
|
|
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
11053
|
// draw axial
|
|
10391
|
-
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])
|
|
10392
11055
|
// draw coronal
|
|
10393
|
-
this.draw2D([ltwh[0], ltwh[1], sX, sZ], 1)
|
|
11056
|
+
this.draw2D([ltwh[0], ltwh[1], sX, sZ], 1, NaN, [actualX, actualZ])
|
|
10394
11057
|
// draw sagittal
|
|
10395
|
-
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])
|
|
10396
11059
|
if (isDraw3D) {
|
|
10397
11060
|
this.draw3D([ltwh[0] + sX + pad, ltwh[1] + sZ + pad, sY, sY])
|
|
10398
11061
|
}
|
|
@@ -10457,12 +11120,44 @@ export class Niivue {
|
|
|
10457
11120
|
width,
|
|
10458
11121
|
height
|
|
10459
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
|
+
}
|
|
10460
11153
|
}
|
|
11154
|
+
|
|
10461
11155
|
const pos = this.frac2mm([this.scene.crosshairPos[0], this.scene.crosshairPos[1], this.scene.crosshairPos[2]])
|
|
10462
11156
|
|
|
10463
11157
|
posString = pos[0].toFixed(2) + '×' + pos[1].toFixed(2) + '×' + pos[2].toFixed(2)
|
|
10464
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)
|
|
10465
11159
|
this.sync()
|
|
11160
|
+
this.drawAnchoredLabels()
|
|
10466
11161
|
return posString
|
|
10467
11162
|
}
|
|
10468
11163
|
|