@niivue/niivue 0.44.2-dev.1 → 0.44.2-dev.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.d.ts +15 -71
- package/build/index.js +198 -460
- package/build/index.js.map +1 -1
- package/build/index.min.js +1 -1
- package/dist/index.d.ts +9 -2
- package/dist/index.js +200 -39
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/package.json +1 -1
- package/src/index_segment.html +79 -11
- package/src/index_sync.html +64 -20
- package/src/niivue/index.ts +261 -49
- package/src/niivue.css +0 -1
- package/src/nvdocument.ts +5 -1
- package/dist/niivue.umd.js +0 -1869
package/src/niivue/index.ts
CHANGED
|
@@ -345,6 +345,9 @@ export class Niivue {
|
|
|
345
345
|
drawUndoBitmaps: Uint8Array[] = [] // array of drawBitmaps for undo
|
|
346
346
|
drawLut = cmapper.makeDrawLut('$itksnap') // the color lookup table for drawing
|
|
347
347
|
drawOpacity = 0.8 // opacity of drawing (default)
|
|
348
|
+
clickToSegmentIsGrowing = false // flag to indicate if the clickToSegment flood fill growing is in progress with left mouse down + drag
|
|
349
|
+
clickToSegmentGrowingBitmap: Uint8Array | null = null // the bitmap of the growing flood fill
|
|
350
|
+
clickToSegmentXY = [0, 0] // the x,y location of the clickToSegment flood fill
|
|
348
351
|
renderDrawAmbientOcclusion = 0.4
|
|
349
352
|
colorbarHeight = 0 // height in pixels, set when colorbar is drawn
|
|
350
353
|
drawPenLocation = [NaN, NaN, NaN]
|
|
@@ -1189,6 +1192,9 @@ export class Niivue {
|
|
|
1189
1192
|
this.drawPenLocation = [NaN, NaN, NaN]
|
|
1190
1193
|
this.drawPenAxCorSag = -1
|
|
1191
1194
|
this.uiData.mousedown = true
|
|
1195
|
+
// reset drag positions used previously.
|
|
1196
|
+
this.setDragStart(0, 0)
|
|
1197
|
+
this.setDragEnd(0, 0)
|
|
1192
1198
|
log.debug('mouse down')
|
|
1193
1199
|
log.debug(e)
|
|
1194
1200
|
// record tile where mouse clicked
|
|
@@ -1198,6 +1204,9 @@ export class Niivue {
|
|
|
1198
1204
|
}
|
|
1199
1205
|
|
|
1200
1206
|
const [x, y] = [pos.x * this.uiData.dpr!, pos.y * this.uiData.dpr!]
|
|
1207
|
+
if (this.opts.clickToSegment) {
|
|
1208
|
+
this.clickToSegmentXY = [x, y]
|
|
1209
|
+
}
|
|
1201
1210
|
const label = this.getLabelAtPoint([x, y])
|
|
1202
1211
|
if (label) {
|
|
1203
1212
|
// check for user defined onclick handler
|
|
@@ -1418,8 +1427,18 @@ export class Niivue {
|
|
|
1418
1427
|
if (this.drawPenFillPts.length > 0) {
|
|
1419
1428
|
this.drawPenFilled()
|
|
1420
1429
|
} else if (this.drawPenAxCorSag >= 0) {
|
|
1421
|
-
this.
|
|
1430
|
+
if (this.opts.clickToSegment) {
|
|
1431
|
+
// copy clickToSegment bitmap to drawBitmap.
|
|
1432
|
+
// determine which bitmap has more non-zero values, indicating it is more recent
|
|
1433
|
+
const sumBitmap = this.sumBitmap(this.drawBitmap)
|
|
1434
|
+
const sumSeg = this.sumBitmap(this.clickToSegmentGrowingBitmap)
|
|
1435
|
+
if (sumSeg > sumBitmap) {
|
|
1436
|
+
this.updateBitmapFromClickToSegment()
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1422
1439
|
}
|
|
1440
|
+
// add the bitmap after the clickToSegment update
|
|
1441
|
+
this.drawAddUndoBitmap()
|
|
1423
1442
|
this.drawPenLocation = [NaN, NaN, NaN]
|
|
1424
1443
|
this.drawPenAxCorSag = -1
|
|
1425
1444
|
if (isFunction(this.onMouseUp)) {
|
|
@@ -1784,6 +1803,40 @@ export class Niivue {
|
|
|
1784
1803
|
if (this.thumbnailVisible) {
|
|
1785
1804
|
return
|
|
1786
1805
|
}
|
|
1806
|
+
// check that the user has actually created an ROI already.
|
|
1807
|
+
const dragStartSum = this.uiData.dragStart.reduce((a, b) => a + b, 0)
|
|
1808
|
+
const dragEndSum = this.uiData.dragEnd.reduce((a, b) => a + b, 0)
|
|
1809
|
+
const validDrag = dragStartSum > 0 && dragEndSum > 0
|
|
1810
|
+
// if dragMode is roiSelection, grow or shrink the selection box
|
|
1811
|
+
// by scrolling the mouse wheel. Grows by 1 pixel per scroll
|
|
1812
|
+
if (this.opts.dragMode === DRAG_MODE.roiSelection && validDrag) {
|
|
1813
|
+
const delta = e.deltaY > 0 ? 1 : -1
|
|
1814
|
+
// update the uiData.dragStart and uiData.dragEnd values to grow or shrink the selection box
|
|
1815
|
+
if (this.uiData.dragStart[0] < this.uiData.dragEnd[0]) {
|
|
1816
|
+
this.uiData.dragStart[0] -= delta
|
|
1817
|
+
this.uiData.dragEnd[0] += delta
|
|
1818
|
+
} else {
|
|
1819
|
+
this.uiData.dragStart[0] += delta
|
|
1820
|
+
this.uiData.dragEnd[0] -= delta
|
|
1821
|
+
}
|
|
1822
|
+
if (this.uiData.dragStart[1] < this.uiData.dragEnd[1]) {
|
|
1823
|
+
this.uiData.dragStart[1] -= delta
|
|
1824
|
+
this.uiData.dragEnd[1] += delta
|
|
1825
|
+
} else {
|
|
1826
|
+
this.uiData.dragStart[1] += delta
|
|
1827
|
+
this.uiData.dragEnd[1] -= delta
|
|
1828
|
+
}
|
|
1829
|
+
// draw the scene
|
|
1830
|
+
this.uiData.isDragging = true // set is dragging so the selection box is drawn for this drawScene call
|
|
1831
|
+
this.drawScene() // drawScene uses isDragging to determine if the selection box or ROI should be drawn
|
|
1832
|
+
this.uiData.isDragging = false // reset is dragging
|
|
1833
|
+
const tileIdx = this.tileIndex(this.uiData.dragStart[0], this.uiData.dragStart[1])
|
|
1834
|
+
this.generateMouseUpCallback(
|
|
1835
|
+
this.screenXY2TextureFrac(this.uiData.dragStart[0], this.uiData.dragStart[1], tileIdx),
|
|
1836
|
+
this.screenXY2TextureFrac(this.uiData.dragEnd[0], this.uiData.dragEnd[1], tileIdx)
|
|
1837
|
+
)
|
|
1838
|
+
return
|
|
1839
|
+
}
|
|
1787
1840
|
const rect = this.canvas!.getBoundingClientRect()
|
|
1788
1841
|
if (e.deltaY < 0) {
|
|
1789
1842
|
this.sliceScroll2D(-0.01, e.clientX - rect.left, e.clientY - rect.top)
|
|
@@ -3382,6 +3435,12 @@ export class Niivue {
|
|
|
3382
3435
|
|
|
3383
3436
|
// not included in public docs
|
|
3384
3437
|
sliceScroll2D(posChange: number, x: number, y: number, isDelta = true): void {
|
|
3438
|
+
// check if the canvas has focus
|
|
3439
|
+
if (this.opts.scrollRequiresFocus && this.canvas !== document.activeElement) {
|
|
3440
|
+
log.warn('Canvas element does not have focus. Scroll events will not be processed.')
|
|
3441
|
+
return
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3385
3444
|
if (this.inGraphTile(x, y)) {
|
|
3386
3445
|
let vol = this.volumes[0].frame4D
|
|
3387
3446
|
if (posChange > 0) {
|
|
@@ -4125,6 +4184,7 @@ export class Niivue {
|
|
|
4125
4184
|
} // something is horribly wrong!
|
|
4126
4185
|
const vx = this.back.dims[1] * this.back.dims[2] * this.back.dims[3]
|
|
4127
4186
|
this.drawBitmap = new Uint8Array(vx)
|
|
4187
|
+
this.clickToSegmentGrowingBitmap = new Uint8Array(vx)
|
|
4128
4188
|
this.drawClearAllUndoBitmaps()
|
|
4129
4189
|
this.drawAddUndoBitmap()
|
|
4130
4190
|
this.drawTexture = this.r8Tex(this.drawTexture, TEXTURE7_DRAW, this.back.dims, true)
|
|
@@ -4660,7 +4720,8 @@ export class Niivue {
|
|
|
4660
4720
|
forceMax = NaN,
|
|
4661
4721
|
neighbors = 6,
|
|
4662
4722
|
// option for only flood filling within max distance from seed voxel
|
|
4663
|
-
maxDistanceMM = Number.POSITIVE_INFINITY
|
|
4723
|
+
maxDistanceMM = Number.POSITIVE_INFINITY,
|
|
4724
|
+
is2D = false
|
|
4664
4725
|
): void {
|
|
4665
4726
|
if (!this.drawBitmap) {
|
|
4666
4727
|
throw new Error('drawBitmap undefined')
|
|
@@ -4682,10 +4743,28 @@ export class Niivue {
|
|
|
4682
4743
|
const nx = dims[0]
|
|
4683
4744
|
const nxy = nx * dims[1]
|
|
4684
4745
|
const nxyz = nxy * dims[2]
|
|
4685
|
-
|
|
4746
|
+
let img = this.drawBitmap.slice()
|
|
4747
|
+
let drawBitmap = this.drawBitmap
|
|
4748
|
+
// use the growing bitmap if the clickToSegmentIsGrowing flag is set.
|
|
4749
|
+
// this allows previewing the flood fill, and the results are copied
|
|
4750
|
+
// to the drawBitmap when the user releases the mouse button.
|
|
4751
|
+
if (this.clickToSegmentIsGrowing) {
|
|
4752
|
+
img = this.clickToSegmentGrowingBitmap.slice()
|
|
4753
|
+
drawBitmap = this.clickToSegmentGrowingBitmap
|
|
4754
|
+
}
|
|
4755
|
+
|
|
4686
4756
|
if (img.length !== nxy * dims[2]) {
|
|
4687
4757
|
return
|
|
4688
4758
|
}
|
|
4759
|
+
// mask2D: axial slices constrained in Z, coronal in Y and sagittal in X
|
|
4760
|
+
let constrainXYZ = -1
|
|
4761
|
+
if (is2D && this.drawPenAxCorSag === SLICE_TYPE.AXIAL) {
|
|
4762
|
+
constrainXYZ = 2
|
|
4763
|
+
} else if (is2D && this.drawPenAxCorSag === SLICE_TYPE.CORONAL) {
|
|
4764
|
+
constrainXYZ = 1
|
|
4765
|
+
} else if (is2D && this.drawPenAxCorSag === SLICE_TYPE.SAGITTAL) {
|
|
4766
|
+
constrainXYZ = 0
|
|
4767
|
+
}
|
|
4689
4768
|
function vx2xyz(vx: number): number[] {
|
|
4690
4769
|
// provided address in 1D array, return XYZ coordinate
|
|
4691
4770
|
const Z = Math.floor(vx / nxy) // slice
|
|
@@ -4705,7 +4784,11 @@ export class Niivue {
|
|
|
4705
4784
|
const maxDistanceMM2 = maxDistanceMM ** 2
|
|
4706
4785
|
// function to check if new point to be checked is less than maxDistanceMM
|
|
4707
4786
|
function isWithinDistance(vx: number): boolean {
|
|
4708
|
-
const
|
|
4787
|
+
const xzyVox = vx2xyz(vx)
|
|
4788
|
+
if (constrainXYZ >= 0 && xzyVox[constrainXYZ] !== seedXYZ[constrainXYZ]) {
|
|
4789
|
+
return false
|
|
4790
|
+
}
|
|
4791
|
+
const xyzMM = vx2mm(xzyVox)
|
|
4709
4792
|
const dist2 = (xyzMM[0] - seedMM[0]) ** 2 + (xyzMM[1] - seedMM[1]) ** 2 + (xyzMM[2] - seedMM[2]) ** 2
|
|
4710
4793
|
return dist2 <= maxDistanceMM2
|
|
4711
4794
|
}
|
|
@@ -4721,7 +4804,7 @@ export class Niivue {
|
|
|
4721
4804
|
}
|
|
4722
4805
|
for (let i = 1; i < nxyz; i++) {
|
|
4723
4806
|
img[i] = 0
|
|
4724
|
-
if (
|
|
4807
|
+
if (drawBitmap[i] === seedColor) {
|
|
4725
4808
|
// check if voxel index i is within maxDistanceMM from seed voxel
|
|
4726
4809
|
if (!isWithinDistance(i)) {
|
|
4727
4810
|
// move to next voxel if not within distance,
|
|
@@ -4775,11 +4858,22 @@ export class Niivue {
|
|
|
4775
4858
|
for (let i = 1; i < nxyz; i++) {
|
|
4776
4859
|
if (img[i] === 2) {
|
|
4777
4860
|
// if part of cluster
|
|
4778
|
-
|
|
4861
|
+
drawBitmap[i] = newColor
|
|
4779
4862
|
}
|
|
4780
4863
|
}
|
|
4781
|
-
this.
|
|
4782
|
-
|
|
4864
|
+
if (!this.clickToSegmentIsGrowing) {
|
|
4865
|
+
this.drawBitmap = drawBitmap.slice()
|
|
4866
|
+
if (!this.opts.clickToSegment) {
|
|
4867
|
+
// only update the undo array if clickToSegment is not enabled.
|
|
4868
|
+
// This avoids adding the same drawing twice, since the mouse up
|
|
4869
|
+
// event will also add the drawing to the undo array.
|
|
4870
|
+
this.drawAddUndoBitmap()
|
|
4871
|
+
}
|
|
4872
|
+
this.refreshDrawing(true, this.clickToSegmentIsGrowing)
|
|
4873
|
+
} else {
|
|
4874
|
+
this.clickToSegmentGrowingBitmap = drawBitmap
|
|
4875
|
+
this.refreshDrawing(true, this.clickToSegmentIsGrowing)
|
|
4876
|
+
}
|
|
4783
4877
|
}
|
|
4784
4878
|
|
|
4785
4879
|
// not included in public docs
|
|
@@ -4966,6 +5060,7 @@ export class Niivue {
|
|
|
4966
5060
|
this.drawClearAllUndoBitmaps()
|
|
4967
5061
|
this.rgbaTex(this.drawTexture, TEXTURE7_DRAW, [2, 2, 2, 2], true)
|
|
4968
5062
|
this.drawBitmap = null
|
|
5063
|
+
this.clickToSegmentGrowingBitmap = null
|
|
4969
5064
|
this.drawScene()
|
|
4970
5065
|
}
|
|
4971
5066
|
|
|
@@ -4975,7 +5070,7 @@ export class Niivue {
|
|
|
4975
5070
|
* @example niivue.refreshDrawing();
|
|
4976
5071
|
* @see {@link https://niivue.github.io/niivue/features/cactus.html | live demo usage}
|
|
4977
5072
|
*/
|
|
4978
|
-
refreshDrawing(isForceRedraw = true): void {
|
|
5073
|
+
refreshDrawing(isForceRedraw = true, useClickToSegmentBitmap = false): void {
|
|
4979
5074
|
if (!this.back?.dims) {
|
|
4980
5075
|
throw new Error('back.dims undefined')
|
|
4981
5076
|
}
|
|
@@ -5005,7 +5100,7 @@ export class Niivue {
|
|
|
5005
5100
|
dims[3],
|
|
5006
5101
|
this.gl.RED,
|
|
5007
5102
|
this.gl.UNSIGNED_BYTE,
|
|
5008
|
-
this.drawBitmap
|
|
5103
|
+
useClickToSegmentBitmap ? this.clickToSegmentGrowingBitmap : this.drawBitmap
|
|
5009
5104
|
)
|
|
5010
5105
|
if (isForceRedraw) {
|
|
5011
5106
|
this.drawScene()
|
|
@@ -5733,43 +5828,99 @@ export class Niivue {
|
|
|
5733
5828
|
mask.fill(0)
|
|
5734
5829
|
console.log('startVox', startVox)
|
|
5735
5830
|
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
5831
|
|
|
5742
|
-
//
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5832
|
+
// identify the constant dimension (the plane where the ellipse is drawn)
|
|
5833
|
+
let constantDim = -1
|
|
5834
|
+
if (startVox[0] === endVox[0]) {
|
|
5835
|
+
constantDim = 0 // x is constant
|
|
5836
|
+
} else if (startVox[1] === endVox[1]) {
|
|
5837
|
+
constantDim = 1 // y is constant
|
|
5838
|
+
} else if (startVox[2] === endVox[2]) {
|
|
5839
|
+
constantDim = 2 // z is constant
|
|
5840
|
+
} else {
|
|
5841
|
+
console.error('Error: No constant dimension found.')
|
|
5842
|
+
return
|
|
5749
5843
|
}
|
|
5750
5844
|
|
|
5845
|
+
// get the varying dimensions
|
|
5846
|
+
const dims = [0, 1, 2]
|
|
5847
|
+
const varDims = dims.filter((dim) => dim !== constantDim)
|
|
5848
|
+
|
|
5849
|
+
// compute the center of the ellipse in voxel coordinates
|
|
5850
|
+
const centerVox = []
|
|
5851
|
+
centerVox[constantDim] = startVox[constantDim]
|
|
5852
|
+
centerVox[varDims[0]] = (startVox[varDims[0]] + endVox[varDims[0]]) / 2
|
|
5853
|
+
centerVox[varDims[1]] = (startVox[varDims[1]] + endVox[varDims[1]]) / 2
|
|
5854
|
+
|
|
5855
|
+
// compute the radii along each varying dimension
|
|
5856
|
+
const radiusX = Math.abs(endVox[varDims[0]] - startVox[varDims[0]]) / 2
|
|
5857
|
+
const radiusY = Math.abs(endVox[varDims[1]] - startVox[varDims[1]]) / 2
|
|
5858
|
+
|
|
5859
|
+
// dimensions of the image
|
|
5751
5860
|
const xdim = hdr.dims[1]
|
|
5752
5861
|
const ydim = hdr.dims[2]
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5862
|
+
// const zdim = hdr.dims[3]
|
|
5863
|
+
|
|
5864
|
+
// define the ranges for the varying dimensions
|
|
5865
|
+
const minVarDim0 = Math.max(0, Math.floor(centerVox[varDims[0]] - radiusX))
|
|
5866
|
+
const maxVarDim0 = Math.min(hdr.dims[varDims[0] + 1] - 1, Math.ceil(centerVox[varDims[0]] + radiusX))
|
|
5867
|
+
|
|
5868
|
+
const minVarDim1 = Math.max(0, Math.floor(centerVox[varDims[1]] - radiusY))
|
|
5869
|
+
const maxVarDim1 = Math.min(hdr.dims[varDims[1] + 1] - 1, Math.ceil(centerVox[varDims[1]] + radiusY))
|
|
5870
|
+
|
|
5871
|
+
// the constant dimension value
|
|
5872
|
+
const constDimVal = centerVox[constantDim]
|
|
5873
|
+
if (constDimVal < 0 || constDimVal >= hdr.dims[constantDim + 1]) {
|
|
5874
|
+
console.error('Error: Constant dimension value is out of bounds.')
|
|
5875
|
+
return
|
|
5876
|
+
}
|
|
5877
|
+
|
|
5878
|
+
// iterate over the varying dimensions and apply the elliptical mask
|
|
5879
|
+
for (let i = minVarDim0; i <= maxVarDim0; i++) {
|
|
5880
|
+
for (let j = minVarDim1; j <= maxVarDim1; j++) {
|
|
5881
|
+
// set the voxel coordinates
|
|
5882
|
+
const voxel = []
|
|
5883
|
+
voxel[constantDim] = constDimVal // Fixed dimension
|
|
5884
|
+
voxel[varDims[0]] = i
|
|
5885
|
+
voxel[varDims[1]] = j
|
|
5886
|
+
// calculate the normalized distances from the center
|
|
5887
|
+
const di = (voxel[varDims[0]] - centerVox[varDims[0]]) / radiusX
|
|
5888
|
+
const dj = (voxel[varDims[1]] - centerVox[varDims[1]]) / radiusY
|
|
5889
|
+
// calculate the squared distance in ellipse space
|
|
5890
|
+
const distSq = di * di + dj * dj
|
|
5891
|
+
// check if the voxel is within the ellipse
|
|
5892
|
+
if (distSq <= 1) {
|
|
5893
|
+
// calculate the index in the mask array
|
|
5894
|
+
const x = voxel[0]
|
|
5895
|
+
const y = voxel[1]
|
|
5896
|
+
const z = voxel[2]
|
|
5897
|
+
const index = z * xdim * ydim + y * xdim + x
|
|
5759
5898
|
mask[index] = 1
|
|
5760
5899
|
}
|
|
5761
5900
|
}
|
|
5762
5901
|
}
|
|
5763
|
-
// calculate
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
const
|
|
5772
|
-
area =
|
|
5902
|
+
// calculate the area based on the number of voxels in the mask
|
|
5903
|
+
const voxelArea = pixDimsRAS[varDims[0] + 1] * pixDimsRAS[varDims[1] + 1] // adjusted for 1-indexing
|
|
5904
|
+
const numMaskedVoxels = mask.reduce((count, value) => count + (value === 1 ? 1 : 0), 0)
|
|
5905
|
+
area = numMaskedVoxels * voxelArea
|
|
5906
|
+
|
|
5907
|
+
// perhaps better to calculate the area using the ellipse area formula
|
|
5908
|
+
const radiusX_mm = radiusX * pixDimsRAS[varDims[0] + 1]
|
|
5909
|
+
const radiusY_mm = radiusY * pixDimsRAS[varDims[1] + 1]
|
|
5910
|
+
const areaEllipse = Math.PI * radiusX_mm * radiusY_mm
|
|
5911
|
+
area = areaEllipse
|
|
5912
|
+
// for debuging: show mask -- loop over drawing and set drawing to 1 if mask is 1
|
|
5913
|
+
// this.setDrawingEnabled(true)
|
|
5914
|
+
// this.drawOpacity = 0.3
|
|
5915
|
+
// for (let i = 0; i < nv; i++) {
|
|
5916
|
+
// if (mask[i] === 1) {
|
|
5917
|
+
// this.drawBitmap![i] = 1
|
|
5918
|
+
// } else {
|
|
5919
|
+
// this.drawBitmap![i] = 0
|
|
5920
|
+
// }
|
|
5921
|
+
// }
|
|
5922
|
+
// this.refreshDrawing(false)
|
|
5923
|
+
// this.setDrawingEnabled(false)
|
|
5773
5924
|
}
|
|
5774
5925
|
// Welfords method
|
|
5775
5926
|
// https://www.embeddedrelated.com/showarticle/785.php
|
|
@@ -7502,6 +7653,31 @@ export class Niivue {
|
|
|
7502
7653
|
return pos[0] > 0 && pos[1] > 0 && pos[0] <= 1 && pos[1] <= 1
|
|
7503
7654
|
}
|
|
7504
7655
|
|
|
7656
|
+
// update drawBitmap if it differs from clickTosegmentBitmap
|
|
7657
|
+
updateBitmapFromClickToSegment(): void {
|
|
7658
|
+
if (this.clickToSegmentGrowingBitmap === null) {
|
|
7659
|
+
return
|
|
7660
|
+
}
|
|
7661
|
+
if (this.drawBitmap === null) {
|
|
7662
|
+
return
|
|
7663
|
+
}
|
|
7664
|
+
if (this.clickToSegmentGrowingBitmap.length !== this.drawBitmap.length) {
|
|
7665
|
+
return
|
|
7666
|
+
}
|
|
7667
|
+
const nvx = this.drawBitmap.length
|
|
7668
|
+
for (let i = 0; i < nvx; i++) {
|
|
7669
|
+
this.drawBitmap[i] = this.clickToSegmentGrowingBitmap[i]
|
|
7670
|
+
}
|
|
7671
|
+
}
|
|
7672
|
+
|
|
7673
|
+
sumBitmap(img: Uint8Array): number {
|
|
7674
|
+
let sum = 0
|
|
7675
|
+
for (let i = 0; i < img.length; i++) {
|
|
7676
|
+
sum += img[i]
|
|
7677
|
+
}
|
|
7678
|
+
return sum
|
|
7679
|
+
}
|
|
7680
|
+
|
|
7505
7681
|
// not included in public docs
|
|
7506
7682
|
// handle mouse click event on canvas
|
|
7507
7683
|
mouseClick(x: number, y: number, posChange = 0, isDelta = true): void {
|
|
@@ -7565,7 +7741,7 @@ export class Niivue {
|
|
|
7565
7741
|
if (axCorSag > SLICE_TYPE.SAGITTAL) {
|
|
7566
7742
|
continue
|
|
7567
7743
|
}
|
|
7568
|
-
|
|
7744
|
+
let texFrac = this.screenXY2TextureFrac(x, y, i, false)
|
|
7569
7745
|
if (texFrac[0] < 0) {
|
|
7570
7746
|
continue
|
|
7571
7747
|
} // click not on slice i
|
|
@@ -7596,21 +7772,37 @@ export class Niivue {
|
|
|
7596
7772
|
}
|
|
7597
7773
|
if (this.opts.drawingEnabled) {
|
|
7598
7774
|
// drawing done in voxels
|
|
7599
|
-
|
|
7775
|
+
let pt = this.frac2vox(this.scene.crosshairPos) as [number, number, number]
|
|
7600
7776
|
// if click-to-segment enabled
|
|
7601
7777
|
if (this.opts.clickToSegment) {
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7778
|
+
texFrac = this.screenXY2TextureFrac(this.clickToSegmentXY[0], this.clickToSegmentXY[1], i, false)
|
|
7779
|
+
pt = this.frac2vox(texFrac) as [number, number, number]
|
|
7780
|
+
let diff = 0
|
|
7781
|
+
let threshold = this.opts.clickToSegmentPercent + diff
|
|
7782
|
+
if (this.uiData.mousedown) {
|
|
7783
|
+
// get percent difference between current x, y, and clickToSegmentXY
|
|
7784
|
+
const xDiff = (this.clickToSegmentXY[0] - x) / this.gl.canvas.width
|
|
7785
|
+
const yDiff = (this.clickToSegmentXY[1] - y) / this.gl.canvas.height
|
|
7786
|
+
diff = Math.max(Math.abs(xDiff), Math.abs(yDiff))
|
|
7787
|
+
// use the sign of the larger difference to determine the direction
|
|
7788
|
+
diff *= Math.sign(Math.abs(xDiff) > Math.abs(yDiff) ? xDiff : yDiff)
|
|
7789
|
+
threshold = this.opts.clickToSegmentPercent + diff
|
|
7607
7790
|
}
|
|
7791
|
+
// get voxel value of pt
|
|
7792
|
+
let voxelIntensity = this.back.getValue(pt[0], pt[1], pt[2])
|
|
7608
7793
|
// if clickToSegmentAutoIntensity, then calculate if we need to flood fill
|
|
7609
7794
|
// in a bright or dark region based on the intensity of the clicked voxel,
|
|
7610
7795
|
// and where it falls in the range of cal_min and cal_max.
|
|
7611
7796
|
// !important! If this option is true, then it will ignore the boolean value of
|
|
7612
7797
|
// clickToSegmentBright supplied by the user
|
|
7613
7798
|
if (this.opts.clickToSegmentAutoIntensity) {
|
|
7799
|
+
if (threshold !== 0) {
|
|
7800
|
+
if (voxelIntensity === 0) {
|
|
7801
|
+
voxelIntensity = 0.01
|
|
7802
|
+
}
|
|
7803
|
+
this.opts.clickToSegmentIntensityMax = voxelIntensity * (1 + threshold)
|
|
7804
|
+
this.opts.clickToSegmentIntensityMin = voxelIntensity * (1 - threshold)
|
|
7805
|
+
}
|
|
7614
7806
|
// if voxel intensity is greater than the midpoint of cal_min and cal_max,
|
|
7615
7807
|
// then flood fill in a bright region
|
|
7616
7808
|
if (voxelIntensity > (this.back.cal_min + this.back.cal_max) * 0.5) {
|
|
@@ -7641,9 +7833,16 @@ export class Niivue {
|
|
|
7641
7833
|
}
|
|
7642
7834
|
// draw the point at the cursor using the penSize to set the
|
|
7643
7835
|
// 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
7836
|
this.drawPt(pt[0], pt[1], pt[2], this.opts.penValue)
|
|
7837
|
+
|
|
7838
|
+
if (diff !== 0) {
|
|
7839
|
+
this.clickToSegmentIsGrowing = true
|
|
7840
|
+
this.clickToSegmentGrowingBitmap = this.drawBitmap.slice()
|
|
7841
|
+
} else {
|
|
7842
|
+
this.clickToSegmentIsGrowing = false
|
|
7843
|
+
}
|
|
7844
|
+
|
|
7845
|
+
// do flood fill
|
|
7647
7846
|
this.drawFloodFill(
|
|
7648
7847
|
[pt[0], pt[1], pt[2]],
|
|
7649
7848
|
0,
|
|
@@ -7651,11 +7850,18 @@ export class Niivue {
|
|
|
7651
7850
|
this.opts.clickToSegmentIntensityMin,
|
|
7652
7851
|
this.opts.clickToSegmentIntensityMax,
|
|
7653
7852
|
this.opts.floodFillNeighbors,
|
|
7654
|
-
this.opts.clickToSegmentMaxDistanceMM
|
|
7853
|
+
this.opts.clickToSegmentMaxDistanceMM,
|
|
7854
|
+
this.opts.clickToSegmentIs2D
|
|
7655
7855
|
)
|
|
7856
|
+
// draw the scene so we see the changes
|
|
7656
7857
|
this.drawScene()
|
|
7657
7858
|
this.createOnLocationChange(axCorSag)
|
|
7658
|
-
|
|
7859
|
+
if (this.clickToSegmentIsGrowing) {
|
|
7860
|
+
// don't get descriptive stats if the user
|
|
7861
|
+
// is still growing the segmented region
|
|
7862
|
+
return
|
|
7863
|
+
}
|
|
7864
|
+
// get the volume of the segmented region if the user is not growing the region
|
|
7659
7865
|
const info = this.getDescriptives({
|
|
7660
7866
|
layer: 0,
|
|
7661
7867
|
masks: [],
|
|
@@ -7928,6 +8134,7 @@ export class Niivue {
|
|
|
7928
8134
|
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4)
|
|
7929
8135
|
this.gl.bindVertexArray(this.unusedVAO) // switch off to avoid tampering with settings
|
|
7930
8136
|
} else {
|
|
8137
|
+
this.drawCircle(leftTopWidthHeight, lineColor, 0.1)
|
|
7931
8138
|
// this.opts.selectionBoxIsOutline == true
|
|
7932
8139
|
this.rectOutlineShader.use(this.gl)
|
|
7933
8140
|
this.gl.enable(this.gl.BLEND)
|
|
@@ -7976,6 +8183,11 @@ export class Niivue {
|
|
|
7976
8183
|
// not included in public docs
|
|
7977
8184
|
// draw a rectangle at desired location
|
|
7978
8185
|
drawSelectionBox(leftTopWidthHeight: number[]): void {
|
|
8186
|
+
if (this.opts.dragMode === DRAG_MODE.roiSelection) {
|
|
8187
|
+
this.drawCircle(leftTopWidthHeight, this.opts.selectionBoxColor, 0.1)
|
|
8188
|
+
return
|
|
8189
|
+
}
|
|
8190
|
+
// else draw a rectangle
|
|
7979
8191
|
this.drawRect(leftTopWidthHeight, this.opts.selectionBoxColor)
|
|
7980
8192
|
}
|
|
7981
8193
|
|
package/src/niivue.css
CHANGED
package/src/nvdocument.ts
CHANGED
|
@@ -150,9 +150,11 @@ export type NVConfigOptions = {
|
|
|
150
150
|
clickToSegmentIntensityMin: number // also covers NaN
|
|
151
151
|
clickToSegmentPercent: number
|
|
152
152
|
clickToSegmentMaxDistanceMM: number // max distance in mm to consider for click to segment flood fill
|
|
153
|
+
clickToSegmentIs2D: boolean
|
|
153
154
|
// selection box outline thickness
|
|
154
155
|
selectionBoxLineThickness: number
|
|
155
156
|
selectionBoxIsOutline: boolean
|
|
157
|
+
scrollRequiresFocus: boolean
|
|
156
158
|
}
|
|
157
159
|
|
|
158
160
|
export const DEFAULT_OPTIONS: NVConfigOptions = {
|
|
@@ -234,8 +236,10 @@ export const DEFAULT_OPTIONS: NVConfigOptions = {
|
|
|
234
236
|
// for the clickToSegmentIntensityMin and clickToSegmentIntensityMax values.
|
|
235
237
|
clickToSegmentPercent: 0,
|
|
236
238
|
clickToSegmentMaxDistanceMM: Number.POSITIVE_INFINITY, // default value is infinity for backwards compatibility with flood fill routine.
|
|
239
|
+
clickToSegmentIs2D: false,
|
|
237
240
|
selectionBoxLineThickness: 4,
|
|
238
|
-
selectionBoxIsOutline: false
|
|
241
|
+
selectionBoxIsOutline: false,
|
|
242
|
+
scrollRequiresFocus: false // determines if the cavas need to be focused to scroll
|
|
239
243
|
}
|
|
240
244
|
|
|
241
245
|
type SceneData = {
|