@niivue/niivue 0.44.2-dev.1 → 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.
@@ -12,7 +12,6 @@ import {
12
12
  vertLineShader,
13
13
  vertLine3DShader,
14
14
  fragRectShader,
15
- fragRectOutlineShader,
16
15
  vertRenderShader,
17
16
  fragRenderShader,
18
17
  fragRenderGradientShader,
@@ -98,6 +97,7 @@ import {
98
97
  ImageFromUrlOptions
99
98
  } from '../nvimage/index.js'
100
99
  import { NVUtilities } from '../nvutilities.js'
100
+ import { NVMeshUtilities } from '../nvmesh-utilities.js'
101
101
  import {
102
102
  Connectome,
103
103
  LegacyConnectome,
@@ -105,8 +105,7 @@ import {
105
105
  NiftiHeader,
106
106
  DragReleaseParams,
107
107
  NiiVueLocation,
108
- NiiVueLocationValue,
109
- SyncOpts
108
+ NiiVueLocationValue
110
109
  } from '../types.js'
111
110
  import {
112
111
  clamp,
@@ -196,7 +195,6 @@ type Descriptive = {
196
195
  cal_max: number
197
196
  robust_min: number
198
197
  robust_max: number
199
- area: number | null
200
198
  }
201
199
 
202
200
  type SliceScale = {
@@ -332,6 +330,7 @@ const defaultSaveImageOptions: SaveImageOptions = {
332
330
  * let niivue = new Niivue({crosshairColor: [0,1,0,0.5], textHeight: 0.5}) // a see-through green crosshair, and larger text labels
333
331
  */
334
332
  export class Niivue {
333
+ loaders = {}
335
334
  canvas: HTMLCanvasElement | null = null // the reference to the canvas element on the page
336
335
  _gl: WebGL2RenderingContext | null = null // the gl context
337
336
  isBusy = false // flag to indicate if the scene is busy drawing
@@ -358,7 +357,6 @@ export class Niivue {
358
357
  orientCubeShader?: Shader
359
358
  orientCubeShaderVAO: WebGLVertexArrayObject | null = null
360
359
  rectShader?: Shader
361
- rectOutlineShader?: Shader
362
360
  renderShader?: Shader
363
361
  lineShader?: Shader
364
362
  line3DShader?: Shader
@@ -404,19 +402,7 @@ export class Niivue {
404
402
  extentsMax?: vec3
405
403
  // ResizeObserver
406
404
  private resizeObserver: ResizeObserver | null = null
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
-
405
+ syncOpts: Record<string, unknown> = {}
420
406
  readyForSync = false
421
407
 
422
408
  // UI Data
@@ -466,7 +452,7 @@ export class Niivue {
466
452
 
467
453
  cuboidVertexBuffer?: WebGLBuffer
468
454
 
469
- otherNV: Niivue[] | null = null // another niivue instance that we wish to sync position with
455
+ otherNV: Niivue | Niivue[] | null = null // another niivue instance that we wish to sync position with
470
456
  volumeObject3D: NiivueObject3D | null = null
471
457
  pivot3D = [0, 0, 0] // center for rendering rotation
472
458
  furthestFromPivot = 10.0 // most distant point from pivot
@@ -935,11 +921,7 @@ export class Niivue {
935
921
  * @deprecated use broadcastTo instead
936
922
  * @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
937
923
  */
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
- }
924
+ syncWith(otherNV: Niivue, syncOpts = { '2d': true, '3d': true }): void {
943
925
  this.otherNV = otherNV
944
926
  this.syncOpts = { ...syncOpts }
945
927
  }
@@ -956,71 +938,10 @@ export class Niivue {
956
938
  * @see {@link https://niivue.github.io/niivue/features/sync.mesh.html | live demo usage}
957
939
  */
958
940
  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
- }
963
941
  this.otherNV = otherNV
964
942
  this.syncOpts = syncOpts
965
943
  }
966
944
 
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
-
1024
945
  /**
1025
946
  * Sync the scene controls (orientation, crosshair location, etc.) from one Niivue instance to another. useful for using one canvas to drive another.
1026
947
  * @internal
@@ -1034,52 +955,58 @@ export class Niivue {
1034
955
  if (!this.gl || !this.otherNV || typeof this.otherNV === 'undefined') {
1035
956
  return
1036
957
  }
958
+ // if (!this.otherNV.readyForSync || !this.readyForSync) {
959
+ // return;
960
+ // }
1037
961
  // canvas must have focus to send messages issue706
1038
962
  if (!(this.gl.canvas as HTMLCanvasElement).matches(':focus')) {
1039
963
  return
1040
964
  }
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
965
+ const thisMM = this.frac2mm(this.scene.crosshairPos)
966
+ // if this.otherNV is an object, then it is a single Niivue instance
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
1074
975
  if (this.syncOpts['2d']) {
1075
- this.doSync2d(this.otherNV[i])
976
+ this.otherNV.scene.crosshairPos = this.otherNV.mm2frac(thisMM)
977
+ this.otherNV.scene.pan2Dxyzmm = vec4.clone(this.scene.pan2Dxyzmm)
1076
978
  }
1077
- // legacy 3d option for multiple properties
979
+ // options specific to 3d rendering
1078
980
  if (this.syncOpts['3d']) {
1079
- this.doSync3d(this.otherNV[i])
981
+ this.otherNV.scene.renderAzimuth = this.scene.renderAzimuth
982
+ this.otherNV.scene.renderElevation = this.scene.renderElevation
983
+ this.otherNV.scene.volScaleMultiplier = this.scene.volScaleMultiplier
984
+ }
985
+ this.otherNV.drawScene()
986
+ this.otherNV.createOnLocationChange()
987
+ } else if (Array.isArray(this.otherNV)) {
988
+ for (let i = 0; i < this.otherNV.length; i++) {
989
+ if (this.otherNV[i] === this) {
990
+ continue
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
+ }
998
+ if (this.syncOpts['2d']) {
999
+ this.otherNV[i].scene.crosshairPos = this.otherNV[i].mm2frac(thisMM)
1000
+ this.otherNV[i].scene.pan2Dxyzmm = vec4.clone(this.scene.pan2Dxyzmm)
1001
+ }
1002
+ if (this.syncOpts['3d']) {
1003
+ this.otherNV[i].scene.renderAzimuth = this.scene.renderAzimuth
1004
+ this.otherNV[i].scene.renderElevation = this.scene.renderElevation
1005
+ this.otherNV[i].scene.volScaleMultiplier = this.scene.volScaleMultiplier
1006
+ }
1007
+ this.otherNV[i].drawScene()
1008
+ this.otherNV[i].createOnLocationChange()
1080
1009
  }
1081
- this.otherNV[i].drawScene()
1082
- this.otherNV[i].createOnLocationChange()
1083
1010
  }
1084
1011
  }
1085
1012
 
@@ -1381,6 +1308,7 @@ export class Niivue {
1381
1308
  const mmLength = vec3.len(v)
1382
1309
  const voxStart = this.frac2vox(fracStart)
1383
1310
  const voxEnd = this.frac2vox(fracEnd)
1311
+
1384
1312
  this.onDragRelease({
1385
1313
  fracStart,
1386
1314
  fracEnd,
@@ -1433,11 +1361,6 @@ export class Niivue {
1433
1361
  const fracStart = this.canvasPos2frac([this.uiData.dragStart[0], this.uiData.dragStart[1]])
1434
1362
  const fracEnd = this.canvasPos2frac([this.uiData.dragEnd[0], this.uiData.dragEnd[1]])
1435
1363
  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
- }
1441
1364
  if (this.opts.dragMode !== DRAG_MODE.contrast) {
1442
1365
  return
1443
1366
  }
@@ -1855,6 +1778,13 @@ export class Niivue {
1855
1778
  if (ext === 'GZ') {
1856
1779
  ext = re.exec(fullname.slice(0, -3))![1] // img.trk.gz -> img.trk
1857
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}`
1858
1788
  }
1859
1789
  return upperCase ? ext : ext.toLowerCase() // developer can choose to have extensions as upper or lower
1860
1790
  }
@@ -1877,6 +1807,33 @@ export class Niivue {
1877
1807
 
1878
1808
  async addVolumesFromUrl(imageOptionsArray: ImageFromUrlOptions[]): Promise<NVImage[]> {
1879
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
+ }
1880
1837
  const imageOptions = {
1881
1838
  url: imageItem.url!,
1882
1839
  headers: imageItem.headers,
@@ -2032,6 +1989,41 @@ export class Niivue {
2032
1989
  })
2033
1990
  }
2034
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
+
2035
2027
  // not included in public docs
2036
2028
  dropListener(e: DragEvent): void {
2037
2029
  e.stopPropagation()
@@ -3881,6 +3873,45 @@ export class Niivue {
3881
3873
  return this.document.download(fileName, compress)
3882
3874
  }
3883
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
+
3884
3915
  /**
3885
3916
  * load an array of volume objects
3886
3917
  * @param volumeList - the array of objects to load. each object must have a resolvable "url" property at a minimum
@@ -3905,30 +3936,33 @@ export class Niivue {
3905
3936
  this.gl.clear(this.gl.COLOR_BUFFER_BIT)
3906
3937
  // if more than one volume, then fetch them all simultaneously
3907
3938
  // using addVolumesFromUrl (note the "s" in "Volumes")
3908
- if (volumeList.length > 1) {
3909
- await this.addVolumesFromUrl(volumeList)
3910
- return this
3911
- }
3912
- const imageOptions = {
3913
- url: volumeList[0].url!,
3914
- headers: volumeList[0].headers,
3915
- name: volumeList[0].name,
3916
- colormap: volumeList[0].colormap ? volumeList[0].colormap : volumeList[0].colorMap,
3917
- colormapNegative: volumeList[0].colormapNegative
3918
- ? volumeList[0].colormapNegative
3919
- : volumeList[0].colorMapNegative,
3920
- opacity: volumeList[0].opacity,
3921
- urlImgData: volumeList[0].urlImgData,
3922
- cal_min: volumeList[0].cal_min,
3923
- cal_max: volumeList[0].cal_max,
3924
- trustCalMinMax: this.opts.trustCalMinMax,
3925
- isManifest: volumeList[0].isManifest,
3926
- frame4D: volumeList[0].frame4D,
3927
- limitFrames4D: volumeList[0].limitFrames4D || this.opts.limitFrames4D,
3928
- colorbarVisible: volumeList[0].colorbarVisible
3929
- }
3930
- await this.addVolumeFromUrl(imageOptions)
3939
+ // if (volumeList.length > 1) {
3940
+ // await this.addVolumesFromUrl(volumeList)
3941
+ // return this
3942
+ // }
3943
+
3944
+ await this.addVolumesFromUrl(volumeList)
3931
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
3932
3966
  }
3933
3967
 
3934
3968
  /**
@@ -3959,7 +3993,34 @@ export class Niivue {
3959
3993
  */
3960
3994
  async addMeshesFromUrl(meshOptions: LoadFromUrlParams[]): Promise<NVMesh[]> {
3961
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.
3962
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
+ }
3963
4024
  if (ext === 'JCON' || ext === 'JSON') {
3964
4025
  const response = await fetch(meshItem.url, {})
3965
4026
  const json = await response.json()
@@ -4008,17 +4069,17 @@ export class Niivue {
4008
4069
 
4009
4070
  // if more than one mesh, then fetch them all simultaneously
4010
4071
  // using addMeshesFromUrl (note the "s" in "Meshes")
4011
- if (meshList.length > 1) {
4012
- await this.addMeshesFromUrl(meshList)
4013
- this.updateGLVolume()
4014
- this.drawScene()
4015
- return this
4016
- }
4017
-
4018
- await this.addMeshFromUrl(meshList[0])
4072
+ // if (meshList.length > 1) {
4073
+ await this.addMeshesFromUrl(meshList)
4019
4074
  this.updateGLVolume()
4020
4075
  this.drawScene()
4021
4076
  return this
4077
+ // }
4078
+
4079
+ // await this.addMeshFromUrl(meshList[0])
4080
+ // this.updateGLVolume()
4081
+ // this.drawScene()
4082
+ // return this
4022
4083
  }
4023
4084
 
4024
4085
  /**
@@ -4313,29 +4374,6 @@ export class Niivue {
4313
4374
  y = Math.min(Math.max(y, 0), dy - 1)
4314
4375
  z = Math.min(Math.max(z, 0), dz - 1)
4315
4376
  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
- }
4339
4377
  }
4340
4378
 
4341
4379
  // not included in public docs
@@ -4422,140 +4460,6 @@ export class Niivue {
4422
4460
  }
4423
4461
  }
4424
4462
 
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
-
4559
4463
  // a voxel can be defined as having 6, 18 or 26 neighbors:
4560
4464
  // 6: neighbors share faces (distance=1)
4561
4465
  // 18: neighbors share faces (distance=1) or edges (1.4)
@@ -4658,9 +4562,7 @@ export class Niivue {
4658
4562
  growSelectedCluster = 0, // if non-zero, growth based on background intensity POSITIVE_INFINITY for selected or bright, NEGATIVE_INFINITY for selected or darker
4659
4563
  forceMin = NaN,
4660
4564
  forceMax = NaN,
4661
- neighbors = 6,
4662
- // option for only flood filling within max distance from seed voxel
4663
- maxDistanceMM = Number.POSITIVE_INFINITY
4565
+ neighbors = 6
4664
4566
  ): void {
4665
4567
  if (!this.drawBitmap) {
4666
4568
  throw new Error('drawBitmap undefined')
@@ -4686,29 +4588,10 @@ export class Niivue {
4686
4588
  if (img.length !== nxy * dims[2]) {
4687
4589
  return
4688
4590
  }
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
- }
4696
4591
  function xyz2vx(pt: number[]): number {
4697
4592
  // provided an XYZ 3D point, provide address in 1D array
4698
4593
  return pt[0] + pt[1] * nx + pt[2] * nxy
4699
4594
  }
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
- }
4712
4595
  const seedVx = xyz2vx(seedXYZ)
4713
4596
  const seedColor = img[seedVx]
4714
4597
  if (seedColor === newColor) {
@@ -4722,12 +4605,6 @@ export class Niivue {
4722
4605
  for (let i = 1; i < nxyz; i++) {
4723
4606
  img[i] = 0
4724
4607
  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
- }
4731
4608
  img[i] = 1
4732
4609
  }
4733
4610
  }
@@ -4759,12 +4636,6 @@ export class Niivue {
4759
4636
  for (let i = 1; i < nxyz; i++) {
4760
4637
  img[i] = 0
4761
4638
  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
- }
4768
4639
  img[i] = 1
4769
4640
  }
4770
4641
  }
@@ -5473,8 +5344,6 @@ export class Niivue {
5473
5344
  // rect shader (crosshair): horizontal and vertical lines only
5474
5345
  this.rectShader = new Shader(gl, vertRectShader, fragRectShader)
5475
5346
  this.rectShader.use(gl)
5476
- this.rectOutlineShader = new Shader(gl, vertRectShader, fragRectOutlineShader)
5477
- this.rectOutlineShader.use(gl)
5478
5347
  // line shader: diagonal lines
5479
5348
  this.lineShader = new Shader(gl, vertLineShader, fragRectShader)
5480
5349
  this.lineShader.use(gl)
@@ -5650,46 +5519,14 @@ export class Niivue {
5650
5519
 
5651
5520
  /**
5652
5521
  * basic statistics for selected voxel-based image
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
- * });
5522
+ * @param layer - selects image to describe
5523
+ * @param masks - are optional binary images to filter voxles
5524
+ * @returns numeric values to describe image
5525
+ * @example niivue.getDescriptives(0);
5670
5526
  * @see {@link https://niivue.github.io/niivue/features/draw2.html | live demo usage}
5671
5527
  */
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
5528
+ getDescriptives(layer = 0, masks = [], drawingIsMask = false): Descriptive {
5691
5529
  const hdr = this.volumes[layer].hdr!
5692
- const pixDimsRAS = this.volumes[layer].pixDimsRAS!
5693
5530
  let slope = hdr.scl_slope
5694
5531
  if (isNaN(slope)) {
5695
5532
  slope = 1
@@ -5728,48 +5565,6 @@ export class Niivue {
5728
5565
  mask[i] = 0
5729
5566
  }
5730
5567
  }
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)
5773
5568
  }
5774
5569
  // Welfords method
5775
5570
  // https://www.embeddedrelated.com/showarticle/785.php
@@ -5830,8 +5625,7 @@ export class Niivue {
5830
5625
  cal_min: this.volumes[layer].cal_min!,
5831
5626
  cal_max: this.volumes[layer].cal_max!,
5832
5627
  robust_min: this.volumes[layer].robust_min!,
5833
- robust_max: this.volumes[layer].robust_max!,
5834
- area
5628
+ robust_max: this.volumes[layer].robust_max!
5835
5629
  }
5836
5630
  }
5837
5631
 
@@ -7597,70 +7391,39 @@ export class Niivue {
7597
7391
  if (this.opts.drawingEnabled) {
7598
7392
  // drawing done in voxels
7599
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)
7600
7396
  // if click-to-segment enabled
7601
7397
  if (this.opts.clickToSegment) {
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
7398
+ const radius = this.opts.clickToSegmentRadius
7399
+ const steps = this.opts.clickToSegmentSteps
7625
7400
  const brightOrDark = this.opts.clickToSegmentBright ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY
7401
+ this.drawPenFillPts = []
7626
7402
  this.drawPenAxCorSag = axCorSag
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)
7403
+ for (let i = 1; i <= steps; i++) {
7404
+ const angle = (i / steps) * 2 * Math.PI
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
7414
+ this.drawPt(xVox, yVox, zVox, this.opts.penValue)
7415
+ this.drawPenFillPts.push([xVox, yVox, pt[2]])
7416
+ // fill in the circle if we are at the last step.
7417
+ // This also triggers the growth of the circle based on cluster intensity method of flood fill.
7418
+ // If the circle is drawn in a bright region, it will grow in the bright region using all connected bright voxels and vice versa.
7419
+ if (i === steps) {
7420
+ this.drawFloodFill([xVox, yVox, pt[2]], 0, brightOrDark, NaN, NaN, this.opts.floodFillNeighbors)
7421
+ }
7641
7422
  }
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
- )
7656
7423
  this.drawScene()
7657
7424
  this.createOnLocationChange(axCorSag)
7658
7425
  // get the volume of the segmented region
7659
- const info = this.getDescriptives({
7660
- layer: 0,
7661
- masks: [],
7662
- drawingIsMask: true
7663
- })
7426
+ const info = this.getDescriptives(0, [], true)
7664
7427
  this.onClickToSegment({ mL: info.volumeML, mm3: info.volumeMM3 })
7665
7428
  return
7666
7429
  }
@@ -7912,43 +7675,20 @@ export class Niivue {
7912
7675
  if (!this.rectShader) {
7913
7676
  throw new Error('rectShader undefined')
7914
7677
  }
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
- }
7678
+ this.rectShader.use(this.gl)
7679
+ this.gl.enable(this.gl.BLEND)
7680
+ this.gl.uniform4fv(this.rectShader.uniforms.lineColor, lineColor)
7681
+ this.gl.uniform2fv(this.rectShader.uniforms.canvasWidthHeight, [this.gl.canvas.width, this.gl.canvas.height])
7682
+ this.gl.uniform4f(
7683
+ this.rectShader.uniforms.leftTopWidthHeight,
7684
+ leftTopWidthHeight[0],
7685
+ leftTopWidthHeight[1],
7686
+ leftTopWidthHeight[2],
7687
+ leftTopWidthHeight[3]
7688
+ )
7689
+ this.gl.bindVertexArray(this.genericVAO)
7690
+ this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4)
7691
+ this.gl.bindVertexArray(this.unusedVAO) // switch off to avoid tampering with settings
7952
7692
  }
7953
7693
 
7954
7694
  drawCircle(leftTopWidthHeight: number[], circleColor = this.opts.fontColor, fillPercent = 1.0): void {
@@ -11120,18 +10860,12 @@ export class Niivue {
11120
10860
  width,
11121
10861
  height
11122
10862
  ])
11123
- return
11124
10863
  }
11125
10864
 
11126
10865
  // draw circle at mouse position if clickToSegment is enabled
11127
10866
  if (this.opts.clickToSegment) {
11128
10867
  const x = this.mousePos[0]
11129
10868
  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
10869
  // determine the tile the mouse is hovering in
11136
10870
  const tileIdx = this.tileIndex(x, y)
11137
10871
  // if a valid tile index, draw the circle