@niivue/niivue 0.44.2 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/build/index.d.ts +31 -6
- package/build/index.js +429 -86
- package/build/index.js.map +1 -1
- package/build/index.min.js +1 -1
- package/dist/index.d.ts +31 -6
- package/dist/index.js +429 -86
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/niivue.umd.js +30 -30
- package/package.json +1 -1
- package/src/cow.iwm.cbor +0 -0
- package/src/fslmean.iwi.cbor +0 -0
- package/src/index.html +47 -8
- package/src/niivue/index.ts +520 -96
- package/src/nvdocument.ts +11 -0
- package/src/nvimage/index.ts +4 -1
- package/src/nvlabel.ts +34 -1
- package/src/nvmesh-loaders.ts +24 -1
package/src/niivue/index.ts
CHANGED
|
@@ -79,7 +79,14 @@ import {
|
|
|
79
79
|
INITIAL_SCENE_DATA
|
|
80
80
|
} from '../nvdocument.js'
|
|
81
81
|
|
|
82
|
-
import {
|
|
82
|
+
import {
|
|
83
|
+
LabelTextAlignment,
|
|
84
|
+
LabelLineTerminator,
|
|
85
|
+
NVLabel3D,
|
|
86
|
+
NVLabel3DStyle,
|
|
87
|
+
LabelAnchorPoint,
|
|
88
|
+
LabelAnchorFlag
|
|
89
|
+
} from '../nvlabel.js'
|
|
83
90
|
import { FreeSurferConnectome, NVConnectome } from '../nvconnectome.js'
|
|
84
91
|
import {
|
|
85
92
|
NVImage,
|
|
@@ -90,6 +97,7 @@ import {
|
|
|
90
97
|
ImageFromUrlOptions
|
|
91
98
|
} from '../nvimage/index.js'
|
|
92
99
|
import { NVUtilities } from '../nvutilities.js'
|
|
100
|
+
import { NVMeshUtilities } from '../nvmesh-utilities.js'
|
|
93
101
|
import {
|
|
94
102
|
Connectome,
|
|
95
103
|
LegacyConnectome,
|
|
@@ -122,7 +130,7 @@ export { NVImage, NVImageFromUrlOptions } from '../nvimage/index.js'
|
|
|
122
130
|
// address rollup error - https://github.com/rollup/plugins/issues/71
|
|
123
131
|
export * from '../nvdocument.js'
|
|
124
132
|
export { NVUtilities } from '../nvutilities.js'
|
|
125
|
-
export { LabelTextAlignment, LabelLineTerminator, NVLabel3DStyle, NVLabel3D } from '../nvlabel.js'
|
|
133
|
+
export { LabelTextAlignment, LabelLineTerminator, NVLabel3DStyle, NVLabel3D, LabelAnchorPoint } from '../nvlabel.js'
|
|
126
134
|
export { NVMeshLoaders } from '../nvmesh-loaders.js'
|
|
127
135
|
export { NVMeshUtilities } from '../nvmesh-utilities.js'
|
|
128
136
|
|
|
@@ -322,6 +330,7 @@ const defaultSaveImageOptions: SaveImageOptions = {
|
|
|
322
330
|
* let niivue = new Niivue({crosshairColor: [0,1,0,0.5], textHeight: 0.5}) // a see-through green crosshair, and larger text labels
|
|
323
331
|
*/
|
|
324
332
|
export class Niivue {
|
|
333
|
+
loaders = {}
|
|
325
334
|
canvas: HTMLCanvasElement | null = null // the reference to the canvas element on the page
|
|
326
335
|
_gl: WebGL2RenderingContext | null = null // the gl context
|
|
327
336
|
isBusy = false // flag to indicate if the scene is busy drawing
|
|
@@ -956,10 +965,18 @@ export class Niivue {
|
|
|
956
965
|
const thisMM = this.frac2mm(this.scene.crosshairPos)
|
|
957
966
|
// if this.otherNV is an object, then it is a single Niivue instance
|
|
958
967
|
if (this.otherNV instanceof Niivue) {
|
|
968
|
+
// gamma not dependent on 2d/3d
|
|
969
|
+
const thisGamma = this.scene.gamma
|
|
970
|
+
const otherGamma = this.otherNV.scene.gamma
|
|
971
|
+
if (thisGamma !== otherGamma) {
|
|
972
|
+
this.otherNV.setGamma(thisGamma)
|
|
973
|
+
}
|
|
974
|
+
// options specific to 2d rendering
|
|
959
975
|
if (this.syncOpts['2d']) {
|
|
960
976
|
this.otherNV.scene.crosshairPos = this.otherNV.mm2frac(thisMM)
|
|
961
977
|
this.otherNV.scene.pan2Dxyzmm = vec4.clone(this.scene.pan2Dxyzmm)
|
|
962
978
|
}
|
|
979
|
+
// options specific to 3d rendering
|
|
963
980
|
if (this.syncOpts['3d']) {
|
|
964
981
|
this.otherNV.scene.renderAzimuth = this.scene.renderAzimuth
|
|
965
982
|
this.otherNV.scene.renderElevation = this.scene.renderElevation
|
|
@@ -972,6 +989,12 @@ export class Niivue {
|
|
|
972
989
|
if (this.otherNV[i] === this) {
|
|
973
990
|
continue
|
|
974
991
|
}
|
|
992
|
+
// gamma not dependent on 2d/3d
|
|
993
|
+
const thisGamma = this.scene.gamma
|
|
994
|
+
const otherGamma = this.otherNV[i].scene.gamma
|
|
995
|
+
if (thisGamma !== otherGamma) {
|
|
996
|
+
this.otherNV[i].setGamma(thisGamma)
|
|
997
|
+
}
|
|
975
998
|
if (this.syncOpts['2d']) {
|
|
976
999
|
this.otherNV[i].scene.crosshairPos = this.otherNV[i].mm2frac(thisMM)
|
|
977
1000
|
this.otherNV[i].scene.pan2Dxyzmm = vec4.clone(this.scene.pan2Dxyzmm)
|
|
@@ -1104,6 +1127,11 @@ export class Niivue {
|
|
|
1104
1127
|
const [x, y] = [pos.x * this.uiData.dpr!, pos.y * this.uiData.dpr!]
|
|
1105
1128
|
const label = this.getLabelAtPoint([x, y])
|
|
1106
1129
|
if (label) {
|
|
1130
|
+
// check for user defined onclick handler
|
|
1131
|
+
if (label.onClick) {
|
|
1132
|
+
label.onClick(label)
|
|
1133
|
+
return
|
|
1134
|
+
}
|
|
1107
1135
|
// find associated mesh
|
|
1108
1136
|
for (const mesh of this.meshes) {
|
|
1109
1137
|
if (mesh.type !== MeshType.CONNECTOME) {
|
|
@@ -1452,6 +1480,16 @@ export class Niivue {
|
|
|
1452
1480
|
this.drawScene()
|
|
1453
1481
|
this.uiData.prevX = this.uiData.currX
|
|
1454
1482
|
this.uiData.prevY = this.uiData.currY
|
|
1483
|
+
} else if (!this.uiData.mousedown && this.opts.clickToSegment) {
|
|
1484
|
+
const pos = this.getNoPaddingNoBorderCanvasRelativeMousePosition(e, this.gl.canvas)
|
|
1485
|
+
// ignore if mouse moves outside of tile of initial click
|
|
1486
|
+
if (!pos) {
|
|
1487
|
+
return
|
|
1488
|
+
}
|
|
1489
|
+
const x = pos.x * this.uiData.dpr!
|
|
1490
|
+
const y = pos.y * this.uiData.dpr!
|
|
1491
|
+
this.mousePos = [x, y]
|
|
1492
|
+
this.drawScene()
|
|
1455
1493
|
}
|
|
1456
1494
|
}
|
|
1457
1495
|
|
|
@@ -1740,6 +1778,13 @@ export class Niivue {
|
|
|
1740
1778
|
if (ext === 'GZ') {
|
|
1741
1779
|
ext = re.exec(fullname.slice(0, -3))![1] // img.trk.gz -> img.trk
|
|
1742
1780
|
ext = ext.toUpperCase()
|
|
1781
|
+
} else if (ext === 'CBOR') {
|
|
1782
|
+
// we want to keep cbor WITH the extension before it.
|
|
1783
|
+
// e.g. if fullname is img.iwi.cbor, we want the ext to be iwi.cbor
|
|
1784
|
+
const endExt = ext
|
|
1785
|
+
ext = re.exec(fullname.slice(0, -5))![1] // img.iwi.cbor -> iwi.cbor
|
|
1786
|
+
ext = ext.toUpperCase()
|
|
1787
|
+
ext = `${ext}.${endExt}`
|
|
1743
1788
|
}
|
|
1744
1789
|
return upperCase ? ext : ext.toLowerCase() // developer can choose to have extensions as upper or lower
|
|
1745
1790
|
}
|
|
@@ -1762,6 +1807,33 @@ export class Niivue {
|
|
|
1762
1807
|
|
|
1763
1808
|
async addVolumesFromUrl(imageOptionsArray: ImageFromUrlOptions[]): Promise<NVImage[]> {
|
|
1764
1809
|
const promises = imageOptionsArray.map(async (imageItem) => {
|
|
1810
|
+
// first check this.loaders to see if the user has
|
|
1811
|
+
// registered a custom loader for this file type.
|
|
1812
|
+
// if so, use that loader to load the file.
|
|
1813
|
+
const ext = this.getFileExt(imageItem.url)
|
|
1814
|
+
if (this.loaders[ext]) {
|
|
1815
|
+
let itemToLoad: string | Uint8Array | ArrayBuffer = imageItem.url
|
|
1816
|
+
const toExt = this.loaders[ext].toExt
|
|
1817
|
+
let name = imageItem.name || imageItem.url
|
|
1818
|
+
// in case the name is a url, just get the basename without the slashes
|
|
1819
|
+
name = name.split('/').pop()
|
|
1820
|
+
// if url is a string fetch the file first
|
|
1821
|
+
if (typeof imageItem.url === 'string') {
|
|
1822
|
+
const url = imageItem.url
|
|
1823
|
+
try {
|
|
1824
|
+
const response = await fetch(url)
|
|
1825
|
+
if (!response.ok) {
|
|
1826
|
+
throw new Error(`Failed to load file: ${response.statusText}`)
|
|
1827
|
+
}
|
|
1828
|
+
itemToLoad = await response.arrayBuffer()
|
|
1829
|
+
} catch (error) {
|
|
1830
|
+
throw new Error(`Failed to load url ${url}: ${error}`)
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
const buffer = await this.loaders[ext].loader(itemToLoad)
|
|
1834
|
+
imageItem.url = buffer
|
|
1835
|
+
imageItem.name = `${name}.${toExt}`
|
|
1836
|
+
}
|
|
1765
1837
|
const imageOptions = {
|
|
1766
1838
|
url: imageItem.url!,
|
|
1767
1839
|
headers: imageItem.headers,
|
|
@@ -1917,6 +1989,41 @@ export class Niivue {
|
|
|
1917
1989
|
})
|
|
1918
1990
|
}
|
|
1919
1991
|
|
|
1992
|
+
/* useLoader
|
|
1993
|
+
|
|
1994
|
+
registers an external file loader for niivue to use when reading files.
|
|
1995
|
+
|
|
1996
|
+
the loader must return an array buffer of the file contents for niivue
|
|
1997
|
+
to parse and the extension of the file so niivue can infer the file type to load.
|
|
1998
|
+
|
|
1999
|
+
example:
|
|
2000
|
+
|
|
2001
|
+
const myCustomLoader = (File) => {
|
|
2002
|
+
// ... do parsing here ...
|
|
2003
|
+
return {
|
|
2004
|
+
arrayBuffer: ArrayBuffer,
|
|
2005
|
+
fileExt: 'iwi.cbor',
|
|
2006
|
+
positions: Float32Array | [], // allows for mesh loading to mz3
|
|
2007
|
+
indices: Uint32Array | [], // allows for mesh loading to mz3
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
nv.useLoader({
|
|
2011
|
+
loader: myCustomLoader,
|
|
2012
|
+
fileExt: 'iwi.cbor',
|
|
2013
|
+
toExt: 'nii'
|
|
2014
|
+
})
|
|
2015
|
+
|
|
2016
|
+
*/
|
|
2017
|
+
useLoader(loader: unknown, fileExt: string, toExt: string): void {
|
|
2018
|
+
this.loaders = {
|
|
2019
|
+
...this.loaders,
|
|
2020
|
+
[fileExt.toUpperCase()]: {
|
|
2021
|
+
loader,
|
|
2022
|
+
toExt
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
|
|
1920
2027
|
// not included in public docs
|
|
1921
2028
|
dropListener(e: DragEvent): void {
|
|
1922
2029
|
e.stopPropagation()
|
|
@@ -3766,6 +3873,45 @@ export class Niivue {
|
|
|
3766
3873
|
return this.document.download(fileName, compress)
|
|
3767
3874
|
}
|
|
3768
3875
|
|
|
3876
|
+
// generic loadImages that wraps loadVolumes and loadMeshes
|
|
3877
|
+
async loadImages(images: Array<ImageFromUrlOptions | LoadFromUrlParams>): Promise<this> {
|
|
3878
|
+
const volumes = []
|
|
3879
|
+
const meshes = []
|
|
3880
|
+
for (const image of images) {
|
|
3881
|
+
if ('url' in image) {
|
|
3882
|
+
const ext = this.getFileExt(image.url)
|
|
3883
|
+
// check this.loaders to see if a user has register
|
|
3884
|
+
// a custom loader for this file extension
|
|
3885
|
+
if (this.loaders[ext]) {
|
|
3886
|
+
// check if the loader type property is a volume or mesh
|
|
3887
|
+
// by using the toExt property
|
|
3888
|
+
const toExt = this.loaders[ext].toExt.toUpperCase()
|
|
3889
|
+
if (MESH_EXTENSIONS.includes(toExt)) {
|
|
3890
|
+
meshes.push(image)
|
|
3891
|
+
} else {
|
|
3892
|
+
volumes.push(image)
|
|
3893
|
+
}
|
|
3894
|
+
// continue to the next image
|
|
3895
|
+
continue
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3898
|
+
if (MESH_EXTENSIONS.includes(ext.toUpperCase())) {
|
|
3899
|
+
meshes.push(image)
|
|
3900
|
+
} else {
|
|
3901
|
+
volumes.push(image)
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
if (volumes.length > 0) {
|
|
3906
|
+
await this.loadVolumes(volumes as ImageFromUrlOptions[])
|
|
3907
|
+
}
|
|
3908
|
+
if (meshes.length > 0) {
|
|
3909
|
+
await this.loadMeshes(meshes as LoadFromUrlParams[])
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
return this
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3769
3915
|
/**
|
|
3770
3916
|
* load an array of volume objects
|
|
3771
3917
|
* @param volumeList - the array of objects to load. each object must have a resolvable "url" property at a minimum
|
|
@@ -3790,30 +3936,33 @@ export class Niivue {
|
|
|
3790
3936
|
this.gl.clear(this.gl.COLOR_BUFFER_BIT)
|
|
3791
3937
|
// if more than one volume, then fetch them all simultaneously
|
|
3792
3938
|
// using addVolumesFromUrl (note the "s" in "Volumes")
|
|
3793
|
-
if (volumeList.length > 1) {
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
}
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
headers: volumeList[0].headers,
|
|
3800
|
-
name: volumeList[0].name,
|
|
3801
|
-
colormap: volumeList[0].colormap ? volumeList[0].colormap : volumeList[0].colorMap,
|
|
3802
|
-
colormapNegative: volumeList[0].colormapNegative
|
|
3803
|
-
? volumeList[0].colormapNegative
|
|
3804
|
-
: volumeList[0].colorMapNegative,
|
|
3805
|
-
opacity: volumeList[0].opacity,
|
|
3806
|
-
urlImgData: volumeList[0].urlImgData,
|
|
3807
|
-
cal_min: volumeList[0].cal_min,
|
|
3808
|
-
cal_max: volumeList[0].cal_max,
|
|
3809
|
-
trustCalMinMax: this.opts.trustCalMinMax,
|
|
3810
|
-
isManifest: volumeList[0].isManifest,
|
|
3811
|
-
frame4D: volumeList[0].frame4D,
|
|
3812
|
-
limitFrames4D: volumeList[0].limitFrames4D || this.opts.limitFrames4D,
|
|
3813
|
-
colorbarVisible: volumeList[0].colorbarVisible
|
|
3814
|
-
}
|
|
3815
|
-
await this.addVolumeFromUrl(imageOptions)
|
|
3939
|
+
// if (volumeList.length > 1) {
|
|
3940
|
+
// await this.addVolumesFromUrl(volumeList)
|
|
3941
|
+
// return this
|
|
3942
|
+
// }
|
|
3943
|
+
|
|
3944
|
+
await this.addVolumesFromUrl(volumeList)
|
|
3816
3945
|
return this
|
|
3946
|
+
// const imageOptions = {
|
|
3947
|
+
// url: volumeList[0].url!,
|
|
3948
|
+
// headers: volumeList[0].headers,
|
|
3949
|
+
// name: volumeList[0].name,
|
|
3950
|
+
// colormap: volumeList[0].colormap ? volumeList[0].colormap : volumeList[0].colorMap,
|
|
3951
|
+
// colormapNegative: volumeList[0].colormapNegative
|
|
3952
|
+
// ? volumeList[0].colormapNegative
|
|
3953
|
+
// : volumeList[0].colorMapNegative,
|
|
3954
|
+
// opacity: volumeList[0].opacity,
|
|
3955
|
+
// urlImgData: volumeList[0].urlImgData,
|
|
3956
|
+
// cal_min: volumeList[0].cal_min,
|
|
3957
|
+
// cal_max: volumeList[0].cal_max,
|
|
3958
|
+
// trustCalMinMax: this.opts.trustCalMinMax,
|
|
3959
|
+
// isManifest: volumeList[0].isManifest,
|
|
3960
|
+
// frame4D: volumeList[0].frame4D,
|
|
3961
|
+
// limitFrames4D: volumeList[0].limitFrames4D || this.opts.limitFrames4D,
|
|
3962
|
+
// colorbarVisible: volumeList[0].colorbarVisible
|
|
3963
|
+
// }
|
|
3964
|
+
// await this.addVolumeFromUrl(imageOptions)
|
|
3965
|
+
// return this
|
|
3817
3966
|
}
|
|
3818
3967
|
|
|
3819
3968
|
/**
|
|
@@ -3844,7 +3993,34 @@ export class Niivue {
|
|
|
3844
3993
|
*/
|
|
3845
3994
|
async addMeshesFromUrl(meshOptions: LoadFromUrlParams[]): Promise<NVMesh[]> {
|
|
3846
3995
|
const promises = meshOptions.map(async (meshItem) => {
|
|
3996
|
+
// first check this.loaders to see if the user has
|
|
3997
|
+
// registered a custom loader for this file type.
|
|
3998
|
+
// if so, use that loader to load the file.
|
|
3847
3999
|
const ext = this.getFileExt(meshItem.url)
|
|
4000
|
+
if (this.loaders[ext]) {
|
|
4001
|
+
let itemToLoad: string | Uint8Array | ArrayBuffer = meshItem.url
|
|
4002
|
+
const toExt = this.loaders[ext].toExt
|
|
4003
|
+
let name = meshItem.name || meshItem.url
|
|
4004
|
+
// in case the name is a url, just get the basename without the slashes
|
|
4005
|
+
name = name.split('/').pop()
|
|
4006
|
+
if (typeof meshItem.url === 'string') {
|
|
4007
|
+
const url = meshItem.url
|
|
4008
|
+
try {
|
|
4009
|
+
const response = await fetch(url)
|
|
4010
|
+
if (!response.ok) {
|
|
4011
|
+
throw new Error(`Failed to load file: ${response.statusText}`)
|
|
4012
|
+
}
|
|
4013
|
+
itemToLoad = await response.arrayBuffer()
|
|
4014
|
+
} catch (error) {
|
|
4015
|
+
throw new Error(`Failed to load url ${url}: ${error}`)
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
const { positions, indices } = await this.loaders[ext].loader(itemToLoad)
|
|
4019
|
+
meshItem.name = `${name}.${toExt}`
|
|
4020
|
+
const mz3 = NVMeshUtilities.createMZ3(positions, indices, false)
|
|
4021
|
+
meshItem.buffer = mz3
|
|
4022
|
+
// return await this.loadFromArrayBuffer(mz3, meshItem.name)
|
|
4023
|
+
}
|
|
3848
4024
|
if (ext === 'JCON' || ext === 'JSON') {
|
|
3849
4025
|
const response = await fetch(meshItem.url, {})
|
|
3850
4026
|
const json = await response.json()
|
|
@@ -3893,17 +4069,17 @@ export class Niivue {
|
|
|
3893
4069
|
|
|
3894
4070
|
// if more than one mesh, then fetch them all simultaneously
|
|
3895
4071
|
// using addMeshesFromUrl (note the "s" in "Meshes")
|
|
3896
|
-
if (meshList.length > 1) {
|
|
3897
|
-
|
|
3898
|
-
this.updateGLVolume()
|
|
3899
|
-
this.drawScene()
|
|
3900
|
-
return this
|
|
3901
|
-
}
|
|
3902
|
-
|
|
3903
|
-
await this.addMeshFromUrl(meshList[0])
|
|
4072
|
+
// if (meshList.length > 1) {
|
|
4073
|
+
await this.addMeshesFromUrl(meshList)
|
|
3904
4074
|
this.updateGLVolume()
|
|
3905
4075
|
this.drawScene()
|
|
3906
4076
|
return this
|
|
4077
|
+
// }
|
|
4078
|
+
|
|
4079
|
+
// await this.addMeshFromUrl(meshList[0])
|
|
4080
|
+
// this.updateGLVolume()
|
|
4081
|
+
// this.drawScene()
|
|
4082
|
+
// return this
|
|
3907
4083
|
}
|
|
3908
4084
|
|
|
3909
4085
|
/**
|
|
@@ -6801,6 +6977,7 @@ export class Niivue {
|
|
|
6801
6977
|
* @see {@link https://niivue.github.io/niivue/features/colormaps.html | live demo usage}
|
|
6802
6978
|
*/
|
|
6803
6979
|
setGamma(gamma = 1.0): void {
|
|
6980
|
+
this.scene.gamma = gamma
|
|
6804
6981
|
cmapper.gamma = gamma
|
|
6805
6982
|
this.updateGLVolume()
|
|
6806
6983
|
}
|
|
@@ -7212,7 +7389,10 @@ export class Niivue {
|
|
|
7212
7389
|
this.scene.crosshairPos = vec3.clone(texFrac)
|
|
7213
7390
|
}
|
|
7214
7391
|
if (this.opts.drawingEnabled) {
|
|
7392
|
+
// drawing done in voxels
|
|
7215
7393
|
const pt = this.frac2vox(this.scene.crosshairPos) as [number, number, number]
|
|
7394
|
+
// radius given in mm
|
|
7395
|
+
const ptMM = this.frac2mm(this.scene.crosshairPos)
|
|
7216
7396
|
// if click-to-segment enabled
|
|
7217
7397
|
if (this.opts.clickToSegment) {
|
|
7218
7398
|
const radius = this.opts.clickToSegmentRadius
|
|
@@ -7222,11 +7402,15 @@ export class Niivue {
|
|
|
7222
7402
|
this.drawPenAxCorSag = axCorSag
|
|
7223
7403
|
for (let i = 1; i <= steps; i++) {
|
|
7224
7404
|
const angle = (i / steps) * 2 * Math.PI
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
const
|
|
7228
|
-
|
|
7229
|
-
|
|
7405
|
+
// get the x,y,z in mm since radius given in mm
|
|
7406
|
+
const xMM = ptMM[0] + radius * Math.cos(angle)
|
|
7407
|
+
const yMM = ptMM[1] + radius * Math.sin(angle)
|
|
7408
|
+
const zMM = ptMM[2]
|
|
7409
|
+
// convert x,y,z in mm to voxels for drawing
|
|
7410
|
+
const xVox = this.back.mm2vox([xMM, yMM, zMM])[0]
|
|
7411
|
+
const yVox = this.back.mm2vox([xMM, yMM, zMM])[1]
|
|
7412
|
+
const zVox = this.back.mm2vox([xMM, yMM, zMM])[2]
|
|
7413
|
+
// draw the point
|
|
7230
7414
|
this.drawPt(xVox, yVox, zVox, this.opts.penValue)
|
|
7231
7415
|
this.drawPenFillPts.push([xVox, yVox, pt[2]])
|
|
7232
7416
|
// fill in the circle if we are at the last step.
|
|
@@ -7556,9 +7740,26 @@ export class Niivue {
|
|
|
7556
7740
|
return labels
|
|
7557
7741
|
}
|
|
7558
7742
|
|
|
7743
|
+
getConnectomeLabels(): NVLabel3D[] {
|
|
7744
|
+
const connectomes = this.meshes.filter((m) => m.type === MeshType.CONNECTOME)
|
|
7745
|
+
const meshNodes = connectomes.flatMap((m) => m.nodes as NVConnectomeNode[])
|
|
7746
|
+
const meshLabels = meshNodes.map((n) => n.label)
|
|
7747
|
+
// filter our undefined labels
|
|
7748
|
+
const definedMeshLabels = meshLabels.filter((l): l is NVLabel3D => l !== undefined)
|
|
7749
|
+
// get all of our non-anchored labels
|
|
7750
|
+
const nonAnchoredLabels = this.document.labels.filter((l) => l.anchor == null || l.anchor === LabelAnchorPoint.NONE)
|
|
7751
|
+
// get the unique set of unanchored labels
|
|
7752
|
+
const nonAnchoredLabelSet = new Set(definedMeshLabels)
|
|
7753
|
+
for (const label of nonAnchoredLabels) {
|
|
7754
|
+
nonAnchoredLabelSet.add(label)
|
|
7755
|
+
}
|
|
7756
|
+
|
|
7757
|
+
return Array.from(nonAnchoredLabelSet)
|
|
7758
|
+
}
|
|
7759
|
+
|
|
7559
7760
|
getBulletMarginWidth(): number {
|
|
7560
7761
|
let bulletMargin = 0
|
|
7561
|
-
const labels = this.
|
|
7762
|
+
const labels = this.getConnectomeLabels()
|
|
7562
7763
|
if (labels.length === 0) {
|
|
7563
7764
|
return 0
|
|
7564
7765
|
}
|
|
@@ -7583,7 +7784,7 @@ export class Niivue {
|
|
|
7583
7784
|
}
|
|
7584
7785
|
|
|
7585
7786
|
getLegendPanelWidth(): number {
|
|
7586
|
-
const labels = this.
|
|
7787
|
+
const labels = this.getConnectomeLabels()
|
|
7587
7788
|
if (!this.opts.showLegend || labels.length === 0) {
|
|
7588
7789
|
return 0
|
|
7589
7790
|
}
|
|
@@ -7614,7 +7815,7 @@ export class Niivue {
|
|
|
7614
7815
|
}
|
|
7615
7816
|
|
|
7616
7817
|
getLegendPanelHeight(): number {
|
|
7617
|
-
const labels = this.
|
|
7818
|
+
const labels = this.getConnectomeLabels()
|
|
7618
7819
|
let height = 0
|
|
7619
7820
|
const scale = 1.0 // we may want to make this adjustable in the future
|
|
7620
7821
|
const verticalMargin = this.opts.textHeight * this.gl.canvas.height * scale
|
|
@@ -8129,7 +8330,11 @@ export class Niivue {
|
|
|
8129
8330
|
|
|
8130
8331
|
// not included in public docs
|
|
8131
8332
|
// show text labels for L/R, A/P, I/S dimensions
|
|
8132
|
-
drawSliceOrientationText(
|
|
8333
|
+
drawSliceOrientationText(
|
|
8334
|
+
leftTopWidthHeight: number[],
|
|
8335
|
+
axCorSag: SLICE_TYPE,
|
|
8336
|
+
padLeftTop: number[] = [NaN, NaN]
|
|
8337
|
+
): void {
|
|
8133
8338
|
this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height)
|
|
8134
8339
|
let topText = 'S'
|
|
8135
8340
|
if (axCorSag === SLICE_TYPE.AXIAL) {
|
|
@@ -8143,9 +8348,32 @@ export class Niivue {
|
|
|
8143
8348
|
this.drawTextRightBelow([leftTopWidthHeight[0], leftTopWidthHeight[1]], leftText + topText)
|
|
8144
8349
|
return
|
|
8145
8350
|
}
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8351
|
+
let drawBelow = true
|
|
8352
|
+
let drawRight = true
|
|
8353
|
+
if (!isNaN(padLeftTop[0])) {
|
|
8354
|
+
const ht = this.opts.textHeight * this.gl.canvas.height + 2
|
|
8355
|
+
if (padLeftTop[1] > ht) {
|
|
8356
|
+
this.drawTextBelow(
|
|
8357
|
+
[leftTopWidthHeight[0] + leftTopWidthHeight[2] * 0.5, leftTopWidthHeight[1] + padLeftTop[1] - ht],
|
|
8358
|
+
topText
|
|
8359
|
+
)
|
|
8360
|
+
drawBelow = false
|
|
8361
|
+
}
|
|
8362
|
+
const wid = this.textWidth(ht, leftText) + 2
|
|
8363
|
+
if (padLeftTop[0] > wid) {
|
|
8364
|
+
this.drawTextRight(
|
|
8365
|
+
[leftTopWidthHeight[0] + padLeftTop[0] - wid, leftTopWidthHeight[1] + leftTopWidthHeight[3] * 0.5],
|
|
8366
|
+
leftText
|
|
8367
|
+
)
|
|
8368
|
+
drawRight = false
|
|
8369
|
+
}
|
|
8370
|
+
}
|
|
8371
|
+
if (drawBelow) {
|
|
8372
|
+
this.drawTextBelow([leftTopWidthHeight[0] + leftTopWidthHeight[2] * 0.5, leftTopWidthHeight[1]], topText)
|
|
8373
|
+
}
|
|
8374
|
+
if (drawRight) {
|
|
8375
|
+
this.drawTextRight([leftTopWidthHeight[0], leftTopWidthHeight[1] + leftTopWidthHeight[3] * 0.5], leftText)
|
|
8376
|
+
}
|
|
8149
8377
|
}
|
|
8150
8378
|
|
|
8151
8379
|
// not included in public docs
|
|
@@ -8188,7 +8416,7 @@ export class Niivue {
|
|
|
8188
8416
|
|
|
8189
8417
|
// not included in public docs
|
|
8190
8418
|
// draw 2D tile
|
|
8191
|
-
|
|
8419
|
+
draw2DMain(leftTopWidthHeight: number[], axCorSag: SLICE_TYPE, customMM = NaN): void {
|
|
8192
8420
|
let frac2mmTexture = this.volumes[0].frac2mm!.slice()
|
|
8193
8421
|
let screen = this.screenFieldOfViewExtendedMM(axCorSag)
|
|
8194
8422
|
let mesh2ortho = mat4.create()
|
|
@@ -8217,8 +8445,10 @@ export class Niivue {
|
|
|
8217
8445
|
elevation = isRadiolgical ? -90 : 90
|
|
8218
8446
|
}
|
|
8219
8447
|
const gl = this.gl
|
|
8448
|
+
let isStretchToScreen = false
|
|
8220
8449
|
if (leftTopWidthHeight[2] === 0 || leftTopWidthHeight[3] === 0) {
|
|
8221
8450
|
// only one tile: stretch tile to fill whole screen.
|
|
8451
|
+
isStretchToScreen = true
|
|
8222
8452
|
const pixPerMMw = gl.canvas.width / screen.fovMM[0]
|
|
8223
8453
|
const pixPerMMh = gl.canvas.height / screen.fovMM[1]
|
|
8224
8454
|
const pixPerMMmin = Math.min(pixPerMMw, pixPerMMh)
|
|
@@ -8377,10 +8607,42 @@ export class Niivue {
|
|
|
8377
8607
|
// no crossbars for mosaic view
|
|
8378
8608
|
this.drawCrosshairs3D(false, 0.15, obj.modelViewProjectionMatrix, true, this.opts.isSliceMM)
|
|
8379
8609
|
}
|
|
8380
|
-
|
|
8610
|
+
if (isStretchToScreen) {
|
|
8611
|
+
// issue1065
|
|
8612
|
+
this.drawSliceOrientationText(leftTopWidthHeight, axCorSag)
|
|
8613
|
+
}
|
|
8381
8614
|
this.readyForSync = true
|
|
8382
8615
|
}
|
|
8383
8616
|
|
|
8617
|
+
draw2D(
|
|
8618
|
+
leftTopWidthHeight: number[],
|
|
8619
|
+
axCorSag: SLICE_TYPE,
|
|
8620
|
+
customMM = NaN,
|
|
8621
|
+
imageWidthHeight: number[] = [NaN, NaN]
|
|
8622
|
+
): void {
|
|
8623
|
+
const padLeftTop = [NaN, NaN]
|
|
8624
|
+
if (isNaN(imageWidthHeight[0])) {
|
|
8625
|
+
this.draw2DMain(leftTopWidthHeight, axCorSag, customMM)
|
|
8626
|
+
} else {
|
|
8627
|
+
// inset as padded in tile
|
|
8628
|
+
const ltwh = leftTopWidthHeight.slice()
|
|
8629
|
+
padLeftTop[0] = Math.floor(0.5 * (ltwh[2] - imageWidthHeight[0]))
|
|
8630
|
+
padLeftTop[1] = Math.floor(0.5 * (ltwh[3] - imageWidthHeight[1]))
|
|
8631
|
+
ltwh[0] += padLeftTop[0]
|
|
8632
|
+
ltwh[1] += padLeftTop[1]
|
|
8633
|
+
ltwh[2] = imageWidthHeight[0]
|
|
8634
|
+
ltwh[3] = imageWidthHeight[1]
|
|
8635
|
+
this.draw2DMain(ltwh, axCorSag, customMM)
|
|
8636
|
+
}
|
|
8637
|
+
if (customMM === Infinity || customMM === -Infinity || axCorSag === SLICE_TYPE.RENDER) {
|
|
8638
|
+
return
|
|
8639
|
+
}
|
|
8640
|
+
if (leftTopWidthHeight[2] !== 0 && leftTopWidthHeight[3] !== 0) {
|
|
8641
|
+
// issue1065
|
|
8642
|
+
this.drawSliceOrientationText(leftTopWidthHeight, axCorSag, padLeftTop)
|
|
8643
|
+
}
|
|
8644
|
+
}
|
|
8645
|
+
|
|
8384
8646
|
// not included in public docs
|
|
8385
8647
|
// determine 3D model view projection matrix
|
|
8386
8648
|
calculateMvpMatrix(_unused: unknown, leftTopWidthHeight = [0, 0, 0, 0], azimuth: number, elevation: number): mat4[] {
|
|
@@ -9079,7 +9341,13 @@ export class Niivue {
|
|
|
9079
9341
|
* @param style - label style
|
|
9080
9342
|
* @param point - 3D point on the model
|
|
9081
9343
|
*/
|
|
9082
|
-
addLabel(
|
|
9344
|
+
addLabel(
|
|
9345
|
+
text: string,
|
|
9346
|
+
style: NVLabel3DStyle,
|
|
9347
|
+
points?: number[] | number[][],
|
|
9348
|
+
anchor?: LabelAnchorPoint,
|
|
9349
|
+
onClick?: (label: NVLabel3D) => void
|
|
9350
|
+
): NVLabel3D {
|
|
9083
9351
|
const defaultStyle = {
|
|
9084
9352
|
textColor: this.opts.legendTextColor,
|
|
9085
9353
|
textScale: 1.0,
|
|
@@ -9091,7 +9359,7 @@ export class Niivue {
|
|
|
9091
9359
|
bulletColor: this.opts.legendTextColor
|
|
9092
9360
|
}
|
|
9093
9361
|
const labelStyle = style ? { ...defaultStyle, ...style } : { ...defaultStyle }
|
|
9094
|
-
const label = new NVLabel3D(text, labelStyle, points)
|
|
9362
|
+
const label = new NVLabel3D(text, { ...labelStyle }, points, anchor, onClick)
|
|
9095
9363
|
this.document.labels.push(label)
|
|
9096
9364
|
return label
|
|
9097
9365
|
}
|
|
@@ -9114,6 +9382,75 @@ export class Niivue {
|
|
|
9114
9382
|
}
|
|
9115
9383
|
|
|
9116
9384
|
getLabelAtPoint(screenPoint: [number, number]): NVLabel3D | null {
|
|
9385
|
+
const scale = 1.0
|
|
9386
|
+
const size = this.opts.textHeight * Math.min(this.gl.canvas.height, this.gl.canvas.width) * scale
|
|
9387
|
+
const verticalMargin = this.opts.textHeight * this.gl.canvas.height * scale
|
|
9388
|
+
|
|
9389
|
+
// get all non-connectome labels
|
|
9390
|
+
for (const label of this.document.labels) {
|
|
9391
|
+
if (label.anchor == null || label.anchor === LabelAnchorPoint.NONE) {
|
|
9392
|
+
continue
|
|
9393
|
+
}
|
|
9394
|
+
|
|
9395
|
+
const labelSize = this.opts.textHeight * this.gl.canvas.height * label.style.textScale
|
|
9396
|
+
const textHeight = this.textHeight(labelSize, label.text)
|
|
9397
|
+
const textWidth = this.textWidth(labelSize, label.text)
|
|
9398
|
+
|
|
9399
|
+
if (label.anchor & LabelAnchorFlag.LEFT) {
|
|
9400
|
+
if (screenPoint[0] > textWidth) {
|
|
9401
|
+
continue
|
|
9402
|
+
}
|
|
9403
|
+
}
|
|
9404
|
+
|
|
9405
|
+
if (label.anchor & LabelAnchorFlag.CENTER) {
|
|
9406
|
+
if (screenPoint[0] < (this.gl.canvas.width - textWidth) / 2) {
|
|
9407
|
+
continue
|
|
9408
|
+
}
|
|
9409
|
+
|
|
9410
|
+
if (screenPoint[0] > (this.gl.canvas.width + textWidth) / 2) {
|
|
9411
|
+
continue
|
|
9412
|
+
}
|
|
9413
|
+
}
|
|
9414
|
+
|
|
9415
|
+
if (label.anchor & LabelAnchorFlag.RIGHT) {
|
|
9416
|
+
if (screenPoint[0] < this.gl.canvas.width - textWidth) {
|
|
9417
|
+
continue
|
|
9418
|
+
}
|
|
9419
|
+
}
|
|
9420
|
+
|
|
9421
|
+
if (label.anchor & LabelAnchorFlag.TOP) {
|
|
9422
|
+
if (screenPoint[1] < verticalMargin / 2) {
|
|
9423
|
+
continue
|
|
9424
|
+
}
|
|
9425
|
+
|
|
9426
|
+
if (screenPoint[1] > textHeight + verticalMargin / 2) {
|
|
9427
|
+
continue
|
|
9428
|
+
}
|
|
9429
|
+
}
|
|
9430
|
+
|
|
9431
|
+
if (label.anchor & LabelAnchorFlag.MIDDLE) {
|
|
9432
|
+
if (screenPoint[1] < (this.gl.canvas.height - textHeight - verticalMargin) / 2) {
|
|
9433
|
+
continue
|
|
9434
|
+
}
|
|
9435
|
+
|
|
9436
|
+
if (screenPoint[1] > (this.gl.canvas.height + textHeight - verticalMargin / 2) / 2) {
|
|
9437
|
+
continue
|
|
9438
|
+
}
|
|
9439
|
+
}
|
|
9440
|
+
|
|
9441
|
+
if (label.anchor & LabelAnchorFlag.BOTTOM) {
|
|
9442
|
+
if (screenPoint[1] < this.gl.canvas.height - textHeight - verticalMargin) {
|
|
9443
|
+
continue
|
|
9444
|
+
}
|
|
9445
|
+
|
|
9446
|
+
if (screenPoint[1] > this.gl.canvas.height - verticalMargin / 2) {
|
|
9447
|
+
continue
|
|
9448
|
+
}
|
|
9449
|
+
}
|
|
9450
|
+
|
|
9451
|
+
// label passed all tests
|
|
9452
|
+
return label
|
|
9453
|
+
}
|
|
9117
9454
|
log.debug('screenPoint', screenPoint)
|
|
9118
9455
|
const panelHeight = this.getLegendPanelHeight()
|
|
9119
9456
|
const panelWidth = this.getLegendPanelWidth()
|
|
@@ -9129,10 +9466,7 @@ export class Niivue {
|
|
|
9129
9466
|
return null
|
|
9130
9467
|
}
|
|
9131
9468
|
|
|
9132
|
-
const
|
|
9133
|
-
const size = this.opts.textHeight * Math.min(this.gl.canvas.height, this.gl.canvas.width) * scale
|
|
9134
|
-
|
|
9135
|
-
const labels = this.getAllLabels()
|
|
9469
|
+
const labels = this.getConnectomeLabels()
|
|
9136
9470
|
for (const label of labels) {
|
|
9137
9471
|
const labelSize = this.opts.textHeight * this.gl.canvas.height * label.style.textScale
|
|
9138
9472
|
const textHeight = this.textHeight(labelSize, label.text)
|
|
@@ -9170,11 +9504,11 @@ export class Niivue {
|
|
|
9170
9504
|
draw3DLabel(
|
|
9171
9505
|
label: NVLabel3D,
|
|
9172
9506
|
pos: vec2,
|
|
9173
|
-
mvpMatrix
|
|
9174
|
-
leftTopWidthHeight
|
|
9175
|
-
bulletMargin
|
|
9176
|
-
legendWidth
|
|
9177
|
-
secondPass
|
|
9507
|
+
mvpMatrix?: mat4,
|
|
9508
|
+
leftTopWidthHeight?: number[],
|
|
9509
|
+
bulletMargin?: number,
|
|
9510
|
+
legendWidth?: number,
|
|
9511
|
+
secondPass?: boolean
|
|
9178
9512
|
): void {
|
|
9179
9513
|
const text = label.text
|
|
9180
9514
|
const left = pos[0]
|
|
@@ -9200,17 +9534,19 @@ export class Niivue {
|
|
|
9200
9534
|
|
|
9201
9535
|
let textLeft = left
|
|
9202
9536
|
|
|
9203
|
-
if (
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
9537
|
+
if (legendWidth) {
|
|
9538
|
+
if (label.style.textAlignment !== LabelTextAlignment.LEFT) {
|
|
9539
|
+
const textWidth = this.textWidth(label.style.textScale, label.text) * size
|
|
9540
|
+
if (label.style.textAlignment === LabelTextAlignment.RIGHT) {
|
|
9541
|
+
textLeft = left + legendWidth - size * 1.5 - textWidth
|
|
9542
|
+
} else {
|
|
9543
|
+
const remaining = legendWidth - (bulletMargin || size)
|
|
9544
|
+
textLeft += (remaining - textWidth) / 2
|
|
9545
|
+
}
|
|
9207
9546
|
} else {
|
|
9208
|
-
|
|
9209
|
-
textLeft +=
|
|
9547
|
+
// textLeft += size / 2;
|
|
9548
|
+
textLeft += bulletMargin
|
|
9210
9549
|
}
|
|
9211
|
-
} else {
|
|
9212
|
-
// textLeft += size / 2;
|
|
9213
|
-
textLeft += bulletMargin
|
|
9214
9550
|
}
|
|
9215
9551
|
|
|
9216
9552
|
this.drawText([textLeft, top], text, label.style.textScale, label.style.textColor)
|
|
@@ -9218,7 +9554,7 @@ export class Niivue {
|
|
|
9218
9554
|
|
|
9219
9555
|
// not included in public docs
|
|
9220
9556
|
draw3DLabels(mvpMatrix: mat4, leftTopWidthHeight: number[], secondPass = false): void {
|
|
9221
|
-
const labels = this.
|
|
9557
|
+
const labels = this.getConnectomeLabels()
|
|
9222
9558
|
if (!this.opts.showLegend || labels.length === 0) {
|
|
9223
9559
|
return
|
|
9224
9560
|
}
|
|
@@ -9268,6 +9604,59 @@ export class Niivue {
|
|
|
9268
9604
|
}
|
|
9269
9605
|
}
|
|
9270
9606
|
|
|
9607
|
+
drawAnchoredLabels(): void {
|
|
9608
|
+
const size = this.opts.textHeight * Math.min(this.gl.canvas.height, this.gl.canvas.width) * 1.0
|
|
9609
|
+
const anchoredLabels = this.document.labels.filter((l) => l.anchor != null && l.anchor !== LabelAnchorPoint.NONE)
|
|
9610
|
+
for (const label of anchoredLabels) {
|
|
9611
|
+
const text = label.text
|
|
9612
|
+
const textHeight = this.textHeight(label.style.textScale, text) * size
|
|
9613
|
+
const textWidth = this.textWidth(label.style.textScale, text) * size
|
|
9614
|
+
let left: number
|
|
9615
|
+
let top: number
|
|
9616
|
+
|
|
9617
|
+
const scale = 1.0 // we may want to make this adjustable in the future
|
|
9618
|
+
const verticalMargin = this.opts.textHeight * this.gl.canvas.height * scale
|
|
9619
|
+
const rectHeightDiff = verticalMargin
|
|
9620
|
+
let rectWidthDiff = verticalMargin / 4
|
|
9621
|
+
let rectHorizontalOffset = 0
|
|
9622
|
+
let rectVerticalOffset = 0
|
|
9623
|
+
|
|
9624
|
+
if (label.anchor & LabelAnchorFlag.LEFT) {
|
|
9625
|
+
left = 0
|
|
9626
|
+
}
|
|
9627
|
+
|
|
9628
|
+
if (label.anchor & LabelAnchorFlag.RIGHT) {
|
|
9629
|
+
left = this.canvas.width - textWidth
|
|
9630
|
+
rectHorizontalOffset -= verticalMargin / 4
|
|
9631
|
+
}
|
|
9632
|
+
|
|
9633
|
+
if (label.anchor & LabelAnchorFlag.CENTER) {
|
|
9634
|
+
left = (this.canvas.width - textWidth) / 2
|
|
9635
|
+
rectHorizontalOffset -= verticalMargin / 4
|
|
9636
|
+
rectWidthDiff += verticalMargin / 4
|
|
9637
|
+
}
|
|
9638
|
+
|
|
9639
|
+
if (label.anchor & LabelAnchorFlag.TOP) {
|
|
9640
|
+
top = 0
|
|
9641
|
+
}
|
|
9642
|
+
|
|
9643
|
+
if (label.anchor & LabelAnchorFlag.MIDDLE) {
|
|
9644
|
+
top = (this.canvas.height - textHeight - verticalMargin) / 2
|
|
9645
|
+
rectVerticalOffset -= verticalMargin / 4
|
|
9646
|
+
}
|
|
9647
|
+
|
|
9648
|
+
if (label.anchor & LabelAnchorFlag.BOTTOM) {
|
|
9649
|
+
top = this.canvas.height - textHeight - verticalMargin
|
|
9650
|
+
rectVerticalOffset -= verticalMargin / 4
|
|
9651
|
+
}
|
|
9652
|
+
this.drawRect(
|
|
9653
|
+
[left + rectHorizontalOffset, top + rectVerticalOffset, textWidth + rectWidthDiff, textHeight + rectHeightDiff],
|
|
9654
|
+
label.style.backgroundColor
|
|
9655
|
+
)
|
|
9656
|
+
this.draw3DLabel(label, [left, top])
|
|
9657
|
+
}
|
|
9658
|
+
}
|
|
9659
|
+
|
|
9271
9660
|
// not included in public docs
|
|
9272
9661
|
draw3D(
|
|
9273
9662
|
leftTopWidthHeight = [0, 0, 0, 0],
|
|
@@ -9360,6 +9749,7 @@ export class Niivue {
|
|
|
9360
9749
|
this.readyForSync = true
|
|
9361
9750
|
this.sync()
|
|
9362
9751
|
this.draw3DLabels(mvpMatrix, relativeLTWH, true)
|
|
9752
|
+
|
|
9363
9753
|
return posString
|
|
9364
9754
|
}
|
|
9365
9755
|
|
|
@@ -10295,7 +10685,14 @@ export class Niivue {
|
|
|
10295
10685
|
}
|
|
10296
10686
|
}
|
|
10297
10687
|
const isDrawPenDown = isFinite(this.drawPenLocation[0]) && this.opts.drawingEnabled
|
|
10688
|
+
|
|
10298
10689
|
const { volScale } = this.sliceScale()
|
|
10690
|
+
const actualScale = volScale.slice()
|
|
10691
|
+
if (this.opts.multiplanarEqualSize) {
|
|
10692
|
+
volScale[0] = 1
|
|
10693
|
+
volScale[1] = 1
|
|
10694
|
+
volScale[2] = 1
|
|
10695
|
+
}
|
|
10299
10696
|
if (typeof this.opts.multiplanarPadPixels !== 'number') {
|
|
10300
10697
|
log.debug('multiplanarPadPixels must be numeric')
|
|
10301
10698
|
}
|
|
@@ -10340,44 +10737,49 @@ export class Niivue {
|
|
|
10340
10737
|
isDrawGrid = true
|
|
10341
10738
|
}
|
|
10342
10739
|
}
|
|
10740
|
+
let ltwh = ltwh2x2
|
|
10343
10741
|
if (isDrawColumn) {
|
|
10344
|
-
|
|
10742
|
+
ltwh = ltwh1x3
|
|
10345
10743
|
if (isShowRender || (this.opts.multiplanarShowRender === SHOW_RENDER.AUTO && ltwh1x4[4] >= ltwh1x3[4])) {
|
|
10346
10744
|
ltwh = ltwh1x4
|
|
10347
10745
|
} else {
|
|
10348
10746
|
isDraw3D = false
|
|
10349
10747
|
}
|
|
10350
|
-
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
10748
|
+
} else if (isDrawRow) {
|
|
10749
|
+
ltwh = ltwh3x1
|
|
10750
|
+
if (isShowRender || (this.opts.multiplanarShowRender === SHOW_RENDER.AUTO && ltwh4x1[4] >= ltwh3x1[4])) {
|
|
10751
|
+
ltwh = ltwh4x1
|
|
10752
|
+
} else {
|
|
10753
|
+
isDraw3D = false
|
|
10754
|
+
}
|
|
10755
|
+
}
|
|
10756
|
+
const sX = volScale[0] * ltwh[4]
|
|
10757
|
+
const sY = volScale[1] * ltwh[4]
|
|
10758
|
+
const sZ = volScale[2] * ltwh[4]
|
|
10759
|
+
const actualX = actualScale[0] * ltwh[4]
|
|
10760
|
+
const actualY = actualScale[1] * ltwh[4]
|
|
10761
|
+
const actualZ = actualScale[2] * ltwh[4]
|
|
10762
|
+
if (isDrawColumn) {
|
|
10354
10763
|
// draw axial
|
|
10355
|
-
this.draw2D([ltwh[0], ltwh[1], sX, sY], 0)
|
|
10764
|
+
this.draw2D([ltwh[0], ltwh[1], sX, sY], 0, NaN, [actualX, actualY])
|
|
10356
10765
|
// draw coronal
|
|
10357
|
-
this.draw2D([ltwh[0], ltwh[1] + sY + pad, sX, sZ], 1)
|
|
10766
|
+
this.draw2D([ltwh[0], ltwh[1] + sY + pad, sX, sZ], 1, NaN, [actualX, actualZ])
|
|
10358
10767
|
// draw sagittal
|
|
10359
|
-
this.draw2D([ltwh[0], ltwh[1] + sY + pad + sZ + pad, sY, sZ], 2)
|
|
10768
|
+
this.draw2D([ltwh[0], ltwh[1] + sY + pad + sZ + pad, sY, sZ], 2, NaN, [actualY, actualZ])
|
|
10360
10769
|
if (isDraw3D) {
|
|
10770
|
+
const sMx = mx * ltwh[4]
|
|
10361
10771
|
this.draw3D([ltwh[0], ltwh[1] + sY + sZ + sZ + pad * 3, sMx, sMx])
|
|
10362
10772
|
}
|
|
10363
10773
|
} else if (isDrawRow) {
|
|
10364
|
-
let ltwh = ltwh3x1
|
|
10365
|
-
if (isShowRender || (this.opts.multiplanarShowRender === SHOW_RENDER.AUTO && ltwh4x1[4] >= ltwh3x1[4])) {
|
|
10366
|
-
ltwh = ltwh4x1
|
|
10367
|
-
} else {
|
|
10368
|
-
isDraw3D = false
|
|
10369
|
-
}
|
|
10370
|
-
const sX = volScale[0] * ltwh[4]
|
|
10371
|
-
const sY = volScale[1] * ltwh[4]
|
|
10372
|
-
const sZ = volScale[2] * ltwh[4]
|
|
10373
10774
|
// draw axial
|
|
10374
|
-
this.draw2D([ltwh[0], ltwh[1], sX, sY], 0)
|
|
10775
|
+
this.draw2D([ltwh[0], ltwh[1], sX, sY], 0, NaN, [actualX, actualY])
|
|
10375
10776
|
// draw coronal
|
|
10376
|
-
this.draw2D([ltwh[0] + sX + pad, ltwh[1], sX, sZ], 1)
|
|
10777
|
+
this.draw2D([ltwh[0] + sX + pad, ltwh[1], sX, sZ], 1, NaN, [actualX, actualZ])
|
|
10377
10778
|
// draw sagittal
|
|
10378
|
-
this.draw2D([ltwh[0] + sX + sX + pad * 2, ltwh[1], sY, sZ], 2)
|
|
10779
|
+
this.draw2D([ltwh[0] + sX + sX + pad * 2, ltwh[1], sY, sZ], 2, NaN, [actualY, actualZ])
|
|
10379
10780
|
if (isDraw3D) {
|
|
10380
|
-
|
|
10781
|
+
const sMx = mx * ltwh[4]
|
|
10782
|
+
this.draw3D([ltwh[0] + sX + sX + sY + pad * 3, ltwh[1], sMx, sMx])
|
|
10381
10783
|
}
|
|
10382
10784
|
} else if (isDrawGrid) {
|
|
10383
10785
|
// did the user turn off 3D render view in multiplanar?
|
|
@@ -10388,16 +10790,12 @@ export class Niivue {
|
|
|
10388
10790
|
if (this.opts.multiplanarShowRender === SHOW_RENDER.AUTO) {
|
|
10389
10791
|
isDraw3D = true
|
|
10390
10792
|
}
|
|
10391
|
-
const ltwh = ltwh2x2
|
|
10392
|
-
const sX = volScale[0] * ltwh[4]
|
|
10393
|
-
const sY = volScale[1] * ltwh[4]
|
|
10394
|
-
const sZ = volScale[2] * ltwh[4]
|
|
10395
10793
|
// draw axial
|
|
10396
|
-
this.draw2D([ltwh[0], ltwh[1] + sZ + pad, sX, sY], 0)
|
|
10794
|
+
this.draw2D([ltwh[0], ltwh[1] + sZ + pad, sX, sY], 0, NaN, [actualX, actualY])
|
|
10397
10795
|
// draw coronal
|
|
10398
|
-
this.draw2D([ltwh[0], ltwh[1], sX, sZ], 1)
|
|
10796
|
+
this.draw2D([ltwh[0], ltwh[1], sX, sZ], 1, NaN, [actualX, actualZ])
|
|
10399
10797
|
// draw sagittal
|
|
10400
|
-
this.draw2D([ltwh[0] + sX + pad, ltwh[1], sY, sZ], 2)
|
|
10798
|
+
this.draw2D([ltwh[0] + sX + pad, ltwh[1], sY, sZ], 2, NaN, [actualY, actualZ])
|
|
10401
10799
|
if (isDraw3D) {
|
|
10402
10800
|
this.draw3D([ltwh[0] + sX + pad, ltwh[1] + sZ + pad, sY, sY])
|
|
10403
10801
|
}
|
|
@@ -10463,11 +10861,37 @@ export class Niivue {
|
|
|
10463
10861
|
height
|
|
10464
10862
|
])
|
|
10465
10863
|
}
|
|
10864
|
+
|
|
10865
|
+
// draw circle at mouse position if clickToSegment is enabled
|
|
10866
|
+
if (this.opts.clickToSegment) {
|
|
10867
|
+
const x = this.mousePos[0]
|
|
10868
|
+
const y = this.mousePos[1]
|
|
10869
|
+
// determine the tile the mouse is hovering in
|
|
10870
|
+
const tileIdx = this.tileIndex(x, y)
|
|
10871
|
+
// if a valid tile index, draw the circle
|
|
10872
|
+
if (tileIdx > -1) {
|
|
10873
|
+
// get fov in mm for this plane presented in the tile
|
|
10874
|
+
const fovMM = this.screenSlices[tileIdx].fovMM
|
|
10875
|
+
// get the left, top, width, height of the tile in pixels
|
|
10876
|
+
const ltwh = this.screenSlices[tileIdx].leftTopWidthHeight
|
|
10877
|
+
// calculate the pixel to mm scale so we can draw the circle
|
|
10878
|
+
// in pixels (so it is highres) but with the radius specified in mm
|
|
10879
|
+
const pixPerMM = ltwh[2] / fovMM[0]
|
|
10880
|
+
// get the crosshair color, but replace the alpha because we want it to be transparent
|
|
10881
|
+
// no matter what. We want to see the image data underneath the circle.
|
|
10882
|
+
const color = this.opts.crosshairColor
|
|
10883
|
+
const segmentCursorColor = [color[0], color[1], color[2], 0.4]
|
|
10884
|
+
const radius = this.opts.clickToSegmentRadius * pixPerMM
|
|
10885
|
+
this.drawCircle([x - radius, y - radius, radius * 2, radius * 2], segmentCursorColor, 1)
|
|
10886
|
+
}
|
|
10887
|
+
}
|
|
10888
|
+
|
|
10466
10889
|
const pos = this.frac2mm([this.scene.crosshairPos[0], this.scene.crosshairPos[1], this.scene.crosshairPos[2]])
|
|
10467
10890
|
|
|
10468
10891
|
posString = pos[0].toFixed(2) + '×' + pos[1].toFixed(2) + '×' + pos[2].toFixed(2)
|
|
10469
10892
|
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)
|
|
10470
10893
|
this.sync()
|
|
10894
|
+
this.drawAnchoredLabels()
|
|
10471
10895
|
return posString
|
|
10472
10896
|
}
|
|
10473
10897
|
|