@niivue/niivue 0.24.2 → 0.24.3
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/dist/niivue.es.js +942 -95
- package/dist/niivue.umd.js +110 -79
- package/package.json +1 -1
- package/src/colortables.js +1 -1
- package/src/indexDraw.html +4 -2
- package/src/niivue.js +460 -48
- package/src/nvimage.js +7 -4
- package/src/nvmesh.js +33 -14
- package/src/shader-srcs.js +39 -8
package/src/niivue.js
CHANGED
|
@@ -51,6 +51,9 @@ import { Log } from "./logger";
|
|
|
51
51
|
import defaultFontPNG from "./fonts/Roboto-Regular.png";
|
|
52
52
|
import defaultFontMetrics from "./fonts/Roboto-Regular.json";
|
|
53
53
|
import { colortables } from "./colortables";
|
|
54
|
+
import { webSocket } from "rxjs/webSocket";
|
|
55
|
+
import { interval } from "rxjs";
|
|
56
|
+
|
|
54
57
|
const log = new Log();
|
|
55
58
|
const cmapper = new colortables();
|
|
56
59
|
|
|
@@ -119,7 +122,9 @@ export function Niivue(options = {}) {
|
|
|
119
122
|
this.volumeTexture = null;
|
|
120
123
|
this.drawTexture = null; //the GPU memory storage of the drawing
|
|
121
124
|
this.drawBitmap = null; //the CPU memory storage of the drawing
|
|
122
|
-
this.
|
|
125
|
+
this.maxDrawUndoBitmaps = 8; //analogy: number of bullets in revolver, we will cycle these
|
|
126
|
+
this.currentDrawUndoBitmap = this.maxDrawUndoBitmaps; //analogy: cylinder position of a revolver
|
|
127
|
+
this.drawUndoBitmaps = [];
|
|
123
128
|
this.drawOpacity = 0.8;
|
|
124
129
|
this.drawPenLocation = [NaN, NaN, NaN];
|
|
125
130
|
this.drawPenAxCorSag = -1; //do not allow pen to drag between Sagittal/Coronal/Axial
|
|
@@ -257,6 +262,14 @@ export function Niivue(options = {}) {
|
|
|
257
262
|
},
|
|
258
263
|
];
|
|
259
264
|
|
|
265
|
+
// multiuser
|
|
266
|
+
this.isController = false;
|
|
267
|
+
this.isInSession = false;
|
|
268
|
+
this.sessionKey = "";
|
|
269
|
+
this.sessionUrl = "";
|
|
270
|
+
this.serverConnection$ = null;
|
|
271
|
+
this.interval$ = null;
|
|
272
|
+
|
|
260
273
|
this.initialized = false;
|
|
261
274
|
// loop through known Niivue properties
|
|
262
275
|
// if the user supplied opts object has a
|
|
@@ -434,6 +447,142 @@ Niivue.prototype.sync = function () {
|
|
|
434
447
|
this.otherNV.drawScene();
|
|
435
448
|
};
|
|
436
449
|
|
|
450
|
+
// Internal function to connect to web socket server
|
|
451
|
+
Niivue.prototype.connectToServer = function (wsServerUrl, sessionName) {
|
|
452
|
+
const url = new URL(wsServerUrl);
|
|
453
|
+
url.pathname = "websockets";
|
|
454
|
+
url.search = "?session=" + sessionName;
|
|
455
|
+
this.serverConnection$ = webSocket(url.href);
|
|
456
|
+
console.log(url.href);
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
// Internal function to schedule updates to the server
|
|
460
|
+
Niivue.prototype.setUpdateInterval = function () {
|
|
461
|
+
this.interval$ = interval(300);
|
|
462
|
+
this.interval$.subscribe(() => {
|
|
463
|
+
this.serverConnection$.next({
|
|
464
|
+
op: "update",
|
|
465
|
+
azimuth: this.scene.renderAzimuth,
|
|
466
|
+
elevation: this.scene.renderElevation,
|
|
467
|
+
clipPlane: this.scene.clipPlane,
|
|
468
|
+
zoom: this.volScaleMultiplier,
|
|
469
|
+
key: this.sessionKey,
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
// Internal function called after a connection with the server has been made
|
|
475
|
+
Niivue.prototype.subscribeToServer = function (
|
|
476
|
+
sessionCreatedCallback,
|
|
477
|
+
sessionJoinedCallback
|
|
478
|
+
) {
|
|
479
|
+
this.serverConnection$.subscribe({
|
|
480
|
+
next: (msg) => {
|
|
481
|
+
switch (msg["op"]) {
|
|
482
|
+
case "update":
|
|
483
|
+
this.scene.renderAzimuth = msg["azimuth"];
|
|
484
|
+
this.scene.renderElevation = msg["elevation"];
|
|
485
|
+
this.volScaleMultiplier = msg["zoom"];
|
|
486
|
+
this.scene.clipPlane = msg["clipPlane"];
|
|
487
|
+
this.drawScene();
|
|
488
|
+
break;
|
|
489
|
+
|
|
490
|
+
case "create":
|
|
491
|
+
console.log(msg);
|
|
492
|
+
if (!msg["isError"]) {
|
|
493
|
+
this.isInSession = true;
|
|
494
|
+
this.sessionKey = msg["key"];
|
|
495
|
+
this.setUpdateInterval();
|
|
496
|
+
}
|
|
497
|
+
if (sessionCreatedCallback) {
|
|
498
|
+
sessionCreatedCallback(
|
|
499
|
+
msg["message"],
|
|
500
|
+
msg["url"],
|
|
501
|
+
msg["key"],
|
|
502
|
+
msg["isError"]
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
break;
|
|
506
|
+
|
|
507
|
+
case "join":
|
|
508
|
+
this.isInSession = true;
|
|
509
|
+
this.isController = msg["isController"];
|
|
510
|
+
if (this.isController) {
|
|
511
|
+
this.setUpdateInterval();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (sessionJoinedCallback) {
|
|
515
|
+
sessionJoinedCallback(
|
|
516
|
+
msg["message"],
|
|
517
|
+
msg["url"],
|
|
518
|
+
msg["isController"]
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
}, // Called whenever there is a message from the server.
|
|
524
|
+
error: (err) => console.log(err), // Called if at any point WebSocket API signals some kind of error.
|
|
525
|
+
complete: () => console.log("complete"), // Called when connection is closed (for whatever reason).
|
|
526
|
+
});
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Create a multiuser session
|
|
531
|
+
* @param {string} wsServerUrl e.g. ws://localhost:3000
|
|
532
|
+
* @param {string} sessionName
|
|
533
|
+
* @param {function(string, string, string, boolean):void} sessionCreatedCallback callback after session has been created with message, session url, session key
|
|
534
|
+
* if there was no error.
|
|
535
|
+
*/
|
|
536
|
+
Niivue.prototype.createSession = function (
|
|
537
|
+
wsServerUrl,
|
|
538
|
+
sessionName,
|
|
539
|
+
sessionCreatedCallback
|
|
540
|
+
) {
|
|
541
|
+
this.connectToServer(wsServerUrl, sessionName);
|
|
542
|
+
|
|
543
|
+
// subscribe to any messages from the server
|
|
544
|
+
this.subscribeToServer(sessionCreatedCallback);
|
|
545
|
+
|
|
546
|
+
// tell the server we want to create a sesion
|
|
547
|
+
this.serverConnection$.next({
|
|
548
|
+
op: "create",
|
|
549
|
+
});
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Join a multiuser session
|
|
554
|
+
* @param {string} wsServerUrl e.g. ws://localhost:3000
|
|
555
|
+
* @param {string} sessionName
|
|
556
|
+
* @param {function(string, string, boolean):void} sessionJoinedCallback callback after session has been joined with message, session url and session key
|
|
557
|
+
*/
|
|
558
|
+
Niivue.prototype.joinSession = function (
|
|
559
|
+
wsServerUrl,
|
|
560
|
+
sessionName,
|
|
561
|
+
key,
|
|
562
|
+
sessionJoinedCallback
|
|
563
|
+
) {
|
|
564
|
+
this.connectToServer(wsServerUrl, sessionName);
|
|
565
|
+
|
|
566
|
+
// subscribe to any messages from the server
|
|
567
|
+
this.subscribeToServer(null, sessionJoinedCallback);
|
|
568
|
+
|
|
569
|
+
// tell the server we want to create a sesion
|
|
570
|
+
this.serverConnection$.next({
|
|
571
|
+
op: "join",
|
|
572
|
+
key: key,
|
|
573
|
+
});
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Close a multiuser session
|
|
578
|
+
*/
|
|
579
|
+
Niivue.prototype.closeSession = function () {
|
|
580
|
+
this.interval$.complete();
|
|
581
|
+
this.serverConnection$.complete();
|
|
582
|
+
this.isInSession = false;
|
|
583
|
+
this.isController = false;
|
|
584
|
+
};
|
|
585
|
+
|
|
437
586
|
/* Not documented publicly for now
|
|
438
587
|
* test if two arrays have equal values for each element
|
|
439
588
|
* @param {Array} a the first array
|
|
@@ -635,6 +784,7 @@ Niivue.prototype.mouseUpListener = function () {
|
|
|
635
784
|
this.scene.mouseButtonRightDown = false;
|
|
636
785
|
this.scene.mouseButtonLeftDown = false;
|
|
637
786
|
if (this.drawPenFillPts.length > 0) this.drawPenFilled();
|
|
787
|
+
else if (this.drawPenAxCorSag >= 0) this.drawAddUndoBitmap();
|
|
638
788
|
this.drawPenLocation = [NaN, NaN, NaN];
|
|
639
789
|
this.drawPenAxCorSag = -1;
|
|
640
790
|
if (this.isDragging) {
|
|
@@ -1196,8 +1346,176 @@ Niivue.prototype.getVolumeIndexByID = function (id) {
|
|
|
1196
1346
|
return -1; // -1 signals that no valid index was found for a volume with the given id
|
|
1197
1347
|
};
|
|
1198
1348
|
|
|
1349
|
+
//inputs
|
|
1350
|
+
// data: Uint8Array to compress
|
|
1351
|
+
//output
|
|
1352
|
+
// returns rle compressed Uint8Array
|
|
1353
|
+
|
|
1354
|
+
function encodeRLE(data) {
|
|
1355
|
+
//https://en.wikipedia.org/wiki/PackBits
|
|
1356
|
+
//run length encoding
|
|
1357
|
+
// input and output are Uint8Array
|
|
1358
|
+
// Will compress data with long runs up to x64
|
|
1359
|
+
// Worst case encoded size is ~1% larger than input
|
|
1360
|
+
let dl = data.length; //input length
|
|
1361
|
+
let dp = 0; //input position
|
|
1362
|
+
//worst case: run length encoding (1+1/127) times larger than input
|
|
1363
|
+
let r = new Uint8Array(dl + Math.ceil(0.01 * dl));
|
|
1364
|
+
let rI = new Int8Array(r.buffer); //typecast as header can be negative
|
|
1365
|
+
let rp = 0; //run length position
|
|
1366
|
+
while (dp < dl) {
|
|
1367
|
+
//for each byte in input
|
|
1368
|
+
let v = data[dp];
|
|
1369
|
+
dp++;
|
|
1370
|
+
let rl = 1; //run length
|
|
1371
|
+
while (rl < 129 && dp < dl && data[dp] === v) {
|
|
1372
|
+
dp++;
|
|
1373
|
+
rl++;
|
|
1374
|
+
} //count run length
|
|
1375
|
+
if (rl > 1) {
|
|
1376
|
+
//header
|
|
1377
|
+
rI[rp] = -rl + 1;
|
|
1378
|
+
rp++;
|
|
1379
|
+
r[rp] = v;
|
|
1380
|
+
rp++;
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
//count literal length
|
|
1384
|
+
while (dp < dl) {
|
|
1385
|
+
if (rl > 127) break;
|
|
1386
|
+
if (dp + 2 < dl) {
|
|
1387
|
+
//console.log(':', v, data[dp], data[dp+1]);
|
|
1388
|
+
if (
|
|
1389
|
+
v !== data[dp] &&
|
|
1390
|
+
data[dp + 2] === data[dp] &&
|
|
1391
|
+
data[dp + 1] === data[dp]
|
|
1392
|
+
)
|
|
1393
|
+
break;
|
|
1394
|
+
}
|
|
1395
|
+
v = data[dp];
|
|
1396
|
+
dp++;
|
|
1397
|
+
rl++;
|
|
1398
|
+
}
|
|
1399
|
+
//write header
|
|
1400
|
+
r[rp] = rl - 1;
|
|
1401
|
+
rp++;
|
|
1402
|
+
for (let i = 0; i < rl; i++) {
|
|
1403
|
+
r[rp] = data[dp - rl + i];
|
|
1404
|
+
rp++;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
log.info("PackBits " + dl + " -> " + rp + " bytes (x" + dl / rp + ")");
|
|
1408
|
+
return r.slice(0, rp);
|
|
1409
|
+
} // encodeRLE()
|
|
1410
|
+
|
|
1411
|
+
//inputs
|
|
1412
|
+
// rle: packbits compressed stream
|
|
1413
|
+
// decodedlen: size of uncompressed data
|
|
1414
|
+
//output
|
|
1415
|
+
// returns Uint8Array of decodedlen bytes
|
|
1416
|
+
function decodeRLE(rle, decodedlen) {
|
|
1417
|
+
let r = new Uint8Array(rle.buffer);
|
|
1418
|
+
let rI = new Int8Array(r.buffer); //typecast as header can be negative
|
|
1419
|
+
let rp = 0; //input position in rle array
|
|
1420
|
+
//d: output uncompressed data array
|
|
1421
|
+
let d = new Uint8Array(decodedlen);
|
|
1422
|
+
let dp = 0; //output position in decoded array
|
|
1423
|
+
while (rp < r.length) {
|
|
1424
|
+
//read header
|
|
1425
|
+
let hdr = rI[rp];
|
|
1426
|
+
rp++;
|
|
1427
|
+
if (hdr < 0) {
|
|
1428
|
+
//write run
|
|
1429
|
+
let v = rI[rp];
|
|
1430
|
+
rp++;
|
|
1431
|
+
for (let i = 0; i < 1 - hdr; i++) {
|
|
1432
|
+
d[dp] = v;
|
|
1433
|
+
dp++;
|
|
1434
|
+
}
|
|
1435
|
+
} else {
|
|
1436
|
+
//write literal
|
|
1437
|
+
for (let i = 0; i < hdr + 1; i++) {
|
|
1438
|
+
d[dp] = rI[rp];
|
|
1439
|
+
rp++;
|
|
1440
|
+
dp++;
|
|
1441
|
+
}
|
|
1442
|
+
} //if run else literal
|
|
1443
|
+
} //while rp < r.length
|
|
1444
|
+
return d;
|
|
1445
|
+
} // decodeRLE()
|
|
1446
|
+
|
|
1447
|
+
/*Niivue.prototype.testRLE = function() {
|
|
1448
|
+
//Demo to test encodeRLE/decodeRLE
|
|
1449
|
+
let len = 256*256*256;
|
|
1450
|
+
let data = new Uint8Array(len);
|
|
1451
|
+
for (let i = 0; i < len; i++)
|
|
1452
|
+
data[i] = ((Math.random() > 0.30) ? 1 : 0);
|
|
1453
|
+
console.log(data);
|
|
1454
|
+
let rle = encodeRLE(data);
|
|
1455
|
+
let ddata = decodeRLE(rle, len);
|
|
1456
|
+
let ok = true;
|
|
1457
|
+
for (let i = 0; i < len; i++)
|
|
1458
|
+
if (data[i] !== ddata[i])
|
|
1459
|
+
ok = false;
|
|
1460
|
+
console.log('decoded correctly:', ok);
|
|
1461
|
+
} // testRLE()*/
|
|
1462
|
+
|
|
1463
|
+
Niivue.prototype.drawAddUndoBitmap = async function (fnm) {
|
|
1464
|
+
if (!this.drawBitmap || this.drawBitmap.length < 1) {
|
|
1465
|
+
console.log("drawAddUndoBitmap error: No drawing open");
|
|
1466
|
+
return false;
|
|
1467
|
+
}
|
|
1468
|
+
//let rle = encodeRLE(this.drawBitmap);
|
|
1469
|
+
//the bitmaps are a cyclical loop, like a revolver hand gun: increment the cylinder
|
|
1470
|
+
this.currentDrawUndoBitmap++;
|
|
1471
|
+
if (this.currentDrawUndoBitmap >= this.maxDrawUndoBitmaps)
|
|
1472
|
+
this.currentDrawUndoBitmap = 0;
|
|
1473
|
+
this.drawUndoBitmaps[this.currentDrawUndoBitmap] = encodeRLE(this.drawBitmap);
|
|
1474
|
+
}; // drawAddUndoBitmap()
|
|
1475
|
+
|
|
1476
|
+
Niivue.prototype.drawClearAllUndoBitmaps = async function () {
|
|
1477
|
+
this.currentDrawUndoBitmap = this.maxDrawUndoBitmaps; //next add will be cylinder 0
|
|
1478
|
+
if (this.drawUndoBitmaps.length < 1) return;
|
|
1479
|
+
for (let i = this.drawUndoBitmaps.length - 1; i >= 0; i--) array[i] = [];
|
|
1480
|
+
}; // drawClearAllUndoBitmaps()
|
|
1481
|
+
|
|
1482
|
+
Niivue.prototype.drawUndo = function () {
|
|
1483
|
+
if (this.drawUndoBitmaps.length < 1) {
|
|
1484
|
+
console.log("undo bitmaps not loaded");
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
//let dims = this.back.hdr.dims;
|
|
1488
|
+
//let nv = dims[1] * dims[2] * dims[3];
|
|
1489
|
+
this.currentDrawUndoBitmap--;
|
|
1490
|
+
if (this.currentDrawUndoBitmap < 0)
|
|
1491
|
+
this.currentDrawUndoBitmap = this.drawUndoBitmaps.length - 1;
|
|
1492
|
+
if (this.currentDrawUndoBitmap >= this.drawUndoBitmaps.length)
|
|
1493
|
+
this.currentDrawUndoBitmap = 0;
|
|
1494
|
+
if (this.drawUndoBitmaps[this.currentDrawUndoBitmap].length < 2) {
|
|
1495
|
+
console.log("drawUndo is misbehaving");
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
this.drawBitmap = decodeRLE(
|
|
1499
|
+
this.drawUndoBitmaps[this.currentDrawUndoBitmap],
|
|
1500
|
+
this.drawBitmap.length
|
|
1501
|
+
);
|
|
1502
|
+
this.refreshDrawing(true);
|
|
1503
|
+
};
|
|
1504
|
+
|
|
1505
|
+
/*Niivue.prototype.drawUndo = function () {
|
|
1506
|
+
let hdr = this.back.hdr;
|
|
1507
|
+
let nv = hdr.dims[1] * hdr.dims[2] * hdr.dims[3];
|
|
1508
|
+
if (this.drawUndoBitmap.length !== nv || this.drawBitmap.length !== nv) {
|
|
1509
|
+
console.log("bitmap dims are wrong");
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
this.drawBitmap = this.drawUndoBitmap.slice();
|
|
1513
|
+
this.refreshDrawing(true);
|
|
1514
|
+
};*/
|
|
1515
|
+
|
|
1199
1516
|
Niivue.prototype.loadDrawing = async function (fnm) {
|
|
1200
1517
|
if (this.drawBitmap) console.log("Overwriting open drawing!");
|
|
1518
|
+
this.drawClearAllUndoBitmaps();
|
|
1201
1519
|
let volume = await NVImage.loadFromUrl({ url: fnm });
|
|
1202
1520
|
//let volume = await NVImage.loadFromFile({file: fnm});
|
|
1203
1521
|
let dims = volume.hdr.dims; //reverse to original
|
|
@@ -1215,7 +1533,7 @@ Niivue.prototype.loadDrawing = async function (fnm) {
|
|
|
1215
1533
|
//if (perm[0] === 1 && perm[1] === 2 && perm[2] === 3) {
|
|
1216
1534
|
let vx = dims[1] * dims[2] * dims[3];
|
|
1217
1535
|
//this.drawBitmap = new Uint8Array(vx);
|
|
1218
|
-
this.drawUndoBitmap = null;
|
|
1536
|
+
//this.drawUndoBitmap = null;
|
|
1219
1537
|
this.drawBitmap = new Uint8Array(vx);
|
|
1220
1538
|
this.drawTexture = this.r8Tex(
|
|
1221
1539
|
this.drawTexture,
|
|
@@ -1273,6 +1591,7 @@ Niivue.prototype.loadDrawing = async function (fnm) {
|
|
|
1273
1591
|
} //for x
|
|
1274
1592
|
} //for y
|
|
1275
1593
|
} //for z
|
|
1594
|
+
this.drawAddUndoBitmap();
|
|
1276
1595
|
this.refreshDrawing(false);
|
|
1277
1596
|
this.drawScene();
|
|
1278
1597
|
};
|
|
@@ -1715,6 +2034,11 @@ Niivue.prototype.setScale = function (scale) {
|
|
|
1715
2034
|
*/
|
|
1716
2035
|
Niivue.prototype.setClipPlaneColor = function (color) {
|
|
1717
2036
|
this.opts.clipPlaneColor = color;
|
|
2037
|
+
this.renderShader.use(this.gl);
|
|
2038
|
+
this.gl.uniform4fv(
|
|
2039
|
+
this.renderShader.uniforms["clipPlaneColor"],
|
|
2040
|
+
this.opts.clipPlaneColor
|
|
2041
|
+
);
|
|
1718
2042
|
this.drawScene();
|
|
1719
2043
|
}; // setClipPlaneColor()
|
|
1720
2044
|
|
|
@@ -1915,7 +2239,9 @@ Niivue.prototype.createEmptyDrawing = function () {
|
|
|
1915
2239
|
if (mn < 1) return; //something is horribly wrong!
|
|
1916
2240
|
let vx = this.back.dims[1] * this.back.dims[2] * this.back.dims[3];
|
|
1917
2241
|
this.drawBitmap = new Uint8Array(vx);
|
|
1918
|
-
this.
|
|
2242
|
+
this.drawClearAllUndoBitmaps();
|
|
2243
|
+
this.drawAddUndoBitmap();
|
|
2244
|
+
//this.drawUndoBitmap = null;
|
|
1919
2245
|
this.drawTexture = this.r8Tex(
|
|
1920
2246
|
this.drawTexture,
|
|
1921
2247
|
this.gl.TEXTURE7,
|
|
@@ -2038,16 +2364,6 @@ function img2ras16(volume) {
|
|
|
2038
2364
|
return img16;
|
|
2039
2365
|
}
|
|
2040
2366
|
|
|
2041
|
-
Niivue.prototype.drawUndo = function () {
|
|
2042
|
-
let hdr = this.back.hdr;
|
|
2043
|
-
let nv = hdr.dims[1] * hdr.dims[2] * hdr.dims[3];
|
|
2044
|
-
if (this.drawUndoBitmap.length !== nv || this.drawBitmap.length !== nv) {
|
|
2045
|
-
console.log("bitmap dims are wrong");
|
|
2046
|
-
return;
|
|
2047
|
-
}
|
|
2048
|
-
this.drawBitmap = this.drawUndoBitmap.slice();
|
|
2049
|
-
this.refreshDrawing(true);
|
|
2050
|
-
};
|
|
2051
2367
|
Niivue.prototype.drawGrowCut = function () {
|
|
2052
2368
|
let hdr = this.back.hdr;
|
|
2053
2369
|
let nv = hdr.dims[1] * hdr.dims[2] * hdr.dims[3];
|
|
@@ -2055,7 +2371,7 @@ Niivue.prototype.drawGrowCut = function () {
|
|
|
2055
2371
|
console.log("bitmap dims are wrong");
|
|
2056
2372
|
return;
|
|
2057
2373
|
}
|
|
2058
|
-
this.drawUndoBitmap = this.drawBitmap.slice();
|
|
2374
|
+
//this.drawUndoBitmap = this.drawBitmap.slice();
|
|
2059
2375
|
let gl = this.gl;
|
|
2060
2376
|
let fb = gl.createFramebuffer();
|
|
2061
2377
|
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
|
|
@@ -2068,15 +2384,17 @@ Niivue.prototype.drawGrowCut = function () {
|
|
|
2068
2384
|
for (let i = 1; i < nv; i++) img16[i] = this.drawBitmap[i];
|
|
2069
2385
|
let label0 = this.r16Tex(null, gl.TEXTURE6, this.back.dims, img16);
|
|
2070
2386
|
let label1 = this.r16Tex(null, gl.TEXTURE7, this.back.dims, img16); //TEXTURE7 = draw Texture
|
|
2071
|
-
|
|
2387
|
+
let kMAX_STRENGTH = 10000;
|
|
2388
|
+
for (let i = 1; i < nv; i++) if (img16[i] > 0) img16[i] = kMAX_STRENGTH;
|
|
2072
2389
|
let strength0 = this.r16Tex(null, gl.TEXTURE4, this.back.dims, img16);
|
|
2073
2390
|
let strength1 = this.r16Tex(null, gl.TEXTURE5, this.back.dims, img16);
|
|
2074
2391
|
this.gl.bindVertexArray(this.genericVAO);
|
|
2075
2392
|
let shader = this.growCutShader;
|
|
2076
2393
|
shader.use(gl);
|
|
2394
|
+
let iterations = 128; //will run 2x this value
|
|
2395
|
+
gl.uniform1i(shader.uniforms["finalPass"], 0);
|
|
2077
2396
|
gl.uniform1i(shader.uniforms["inputTexture0"], 3); // background is TEXTURE3
|
|
2078
|
-
|
|
2079
|
-
for (let j = 0; j < 45; j++) {
|
|
2397
|
+
for (let j = 0; j < iterations; j++) {
|
|
2080
2398
|
gl.uniform1i(shader.uniforms["inputTexture1"], 6); // label0 is TEXTURE6
|
|
2081
2399
|
gl.uniform1i(shader.uniforms["inputTexture2"], 4); // strength0 is TEXTURE4
|
|
2082
2400
|
for (let i = 0; i < this.back.dims[3]; i++) {
|
|
@@ -2104,6 +2422,7 @@ Niivue.prototype.drawGrowCut = function () {
|
|
|
2104
2422
|
this.gl.drawArrays(this.gl.TRIANGLE_STRIP, 0, 4);
|
|
2105
2423
|
} //for i: each slice
|
|
2106
2424
|
//reverse order strength1/label1 and strength0/label0 for reading and writing:
|
|
2425
|
+
if (j === iterations - 1) gl.uniform1i(shader.uniforms["finalPass"], 1);
|
|
2107
2426
|
gl.uniform1i(shader.uniforms["inputTexture1"], 7); // label1 is TEXTURE7
|
|
2108
2427
|
gl.uniform1i(shader.uniforms["inputTexture2"], 5); // strength1 is TEXTURE5
|
|
2109
2428
|
for (let i = 0; i < this.back.dims[3]; i++) {
|
|
@@ -2132,17 +2451,9 @@ Niivue.prototype.drawGrowCut = function () {
|
|
|
2132
2451
|
} //for j: each iteration
|
|
2133
2452
|
//read data
|
|
2134
2453
|
gl.drawBuffers([gl.COLOR_ATTACHMENT0]);
|
|
2135
|
-
let readAttach = gl.
|
|
2136
|
-
let readTex =
|
|
2137
|
-
let readStrength = false;
|
|
2138
|
-
if (readStrength) {
|
|
2139
|
-
//read strength or label
|
|
2140
|
-
//readAttach = gl.COLOR_ATTACHMENT1;
|
|
2141
|
-
readTex = strength1;
|
|
2142
|
-
}
|
|
2143
|
-
//gl.readBuffer(gl.COLOR_ATTACHMENT1); //strength
|
|
2454
|
+
let readAttach = gl.COLOR_ATTACHMENT1;
|
|
2455
|
+
let readTex = label0;
|
|
2144
2456
|
gl.readBuffer(readAttach); //label
|
|
2145
|
-
|
|
2146
2457
|
// assuming a framebuffer is bound with the texture to read attached
|
|
2147
2458
|
const format = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_FORMAT);
|
|
2148
2459
|
const type = gl.getParameter(gl.IMPLEMENTATION_COLOR_READ_TYPE);
|
|
@@ -2173,12 +2484,7 @@ Niivue.prototype.drawGrowCut = function () {
|
|
|
2173
2484
|
}
|
|
2174
2485
|
let mx = img16[0];
|
|
2175
2486
|
for (let i = 0; i < img16.length; i++) mx = Math.max(mx, img16[i]);
|
|
2176
|
-
//let img16 = new Int16Array(nv);
|
|
2177
|
-
//gl.readPixels(0, 0, w, h, format, type);
|
|
2178
|
-
// this.fallbackReadPixelsFormat, this.fallbackReadPixelsType, fallbackSliceView);
|
|
2179
|
-
|
|
2180
2487
|
for (let i = 1; i < nv; i++) this.drawBitmap[i] = img16[i];
|
|
2181
|
-
|
|
2182
2488
|
//clean up
|
|
2183
2489
|
//restore textures
|
|
2184
2490
|
gl.activeTexture(gl.TEXTURE2);
|
|
@@ -2199,6 +2505,7 @@ Niivue.prototype.drawGrowCut = function () {
|
|
|
2199
2505
|
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
|
2200
2506
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
2201
2507
|
gl.deleteFramebuffer(fb);
|
|
2508
|
+
this.drawAddUndoBitmap();
|
|
2202
2509
|
this.refreshDrawing(true);
|
|
2203
2510
|
};
|
|
2204
2511
|
|
|
@@ -2289,6 +2596,87 @@ Niivue.prototype.drawLine = function (ptA, ptB, penValue) {
|
|
|
2289
2596
|
}
|
|
2290
2597
|
};
|
|
2291
2598
|
|
|
2599
|
+
Niivue.prototype.drawFloodFill = function (seedXYZ, newColor = 0) {
|
|
2600
|
+
//3D "paint bucket" fill:
|
|
2601
|
+
// set all voxels connected to seed point to newColor
|
|
2602
|
+
// https://en.wikipedia.org/wiki/Flood_fill
|
|
2603
|
+
newColor = Math.abs(newColor);
|
|
2604
|
+
let dims = [this.back.dims[1], this.back.dims[2], this.back.dims[3]]; //+1: dims indexed from 0!
|
|
2605
|
+
if (seedXYZ[0] < 0 || seedXYZ[1] < 0 || seedXYZ[2] < 0) return;
|
|
2606
|
+
if (seedXYZ[0] >= dims[0] || seedXYZ[1] >= dims[1] || seedXYZ[2] >= dims[2])
|
|
2607
|
+
return;
|
|
2608
|
+
let nx = dims[0];
|
|
2609
|
+
let nxy = nx * dims[1];
|
|
2610
|
+
let nxyz = nxy * dims[2];
|
|
2611
|
+
let img = this.drawBitmap.slice();
|
|
2612
|
+
if (img.length !== nxy * dims[2]) return;
|
|
2613
|
+
function xyz2vx(pt) {
|
|
2614
|
+
//provided an XYZ 3D point, provide address in 1D array
|
|
2615
|
+
return pt[0] + pt[1] * nx + pt[2] * nxy;
|
|
2616
|
+
}
|
|
2617
|
+
function vx2xyz(vx) {
|
|
2618
|
+
//provided address in 1D array, return XYZ coordinate
|
|
2619
|
+
let Z = Math.floor(vx / nxy); //slice
|
|
2620
|
+
let Y = Math.floor((vx - Z * nxy) / nx); //column
|
|
2621
|
+
let X = Math.floor(vx % nx);
|
|
2622
|
+
return [X, Y, Z];
|
|
2623
|
+
}
|
|
2624
|
+
let seedVx = xyz2vx(seedXYZ);
|
|
2625
|
+
let seedColor = img[seedVx];
|
|
2626
|
+
if (seedColor === newColor) {
|
|
2627
|
+
console.log("drawFloodFill selected voxel is already desired color");
|
|
2628
|
+
return;
|
|
2629
|
+
}
|
|
2630
|
+
//img is binary mask with selected color as 1 and all other colors 0
|
|
2631
|
+
for (let i = 1; i < nxyz; i++) {
|
|
2632
|
+
img[i] = 0;
|
|
2633
|
+
if (this.drawBitmap[i] === seedColor) img[i] = 1;
|
|
2634
|
+
}
|
|
2635
|
+
//1. Set Q to the empty queue or stack.
|
|
2636
|
+
let Q = [];
|
|
2637
|
+
//2. Add node to the end of Q.
|
|
2638
|
+
Q.push(seedVx);
|
|
2639
|
+
img[seedVx] = 2; //part of cluster
|
|
2640
|
+
//3. While Q is not empty:
|
|
2641
|
+
while (Q.length > 0) {
|
|
2642
|
+
//4. Set n equal to the first element of Q.
|
|
2643
|
+
let vx = Q[0];
|
|
2644
|
+
//5. Remove first element from Q.
|
|
2645
|
+
Q.shift();
|
|
2646
|
+
//6. Test six neighbors of n (left,right,anterior,posterior,inferior, superior
|
|
2647
|
+
// If any is is unfound part of cluster (value = 1) set it to found (value 2) and add to Q
|
|
2648
|
+
let xyz = vx2xyz(vx);
|
|
2649
|
+
function testNeighbor(offset) {
|
|
2650
|
+
let xyzN = xyz.slice();
|
|
2651
|
+
xyzN[0] += offset[0];
|
|
2652
|
+
xyzN[1] += offset[1];
|
|
2653
|
+
xyzN[2] += offset[2];
|
|
2654
|
+
if (xyzN[0] < 0 || xyzN[1] < 0 || xyzN[2] < 0) return;
|
|
2655
|
+
if (xyzN[0] >= dims[0] || xyzN[1] >= dims[1] || xyzN[2] >= dims[2])
|
|
2656
|
+
return;
|
|
2657
|
+
let vxT = xyz2vx(xyzN);
|
|
2658
|
+
if (img[vxT] !== 1) return;
|
|
2659
|
+
img[vxT] = 2; //part of cluster
|
|
2660
|
+
Q.push(vxT);
|
|
2661
|
+
}
|
|
2662
|
+
testNeighbor([0, 0, -1]); //inferior
|
|
2663
|
+
testNeighbor([0, 0, 1]); //superior
|
|
2664
|
+
testNeighbor([0, -1, 0]); //posterior
|
|
2665
|
+
testNeighbor([0, 1, 0]); //anterior
|
|
2666
|
+
testNeighbor([-1, 0, 0]); //left
|
|
2667
|
+
testNeighbor([1, 0, 0]); //right
|
|
2668
|
+
//7. Continue looping until Q is exhausted.
|
|
2669
|
+
}
|
|
2670
|
+
//8. Return
|
|
2671
|
+
//this.drawUndoBitmap = this.drawBitmap.slice();
|
|
2672
|
+
for (let i = 1; i < nxyz; i++)
|
|
2673
|
+
if (img[i] === 2)
|
|
2674
|
+
//if part of cluster
|
|
2675
|
+
this.drawBitmap[i] = newColor;
|
|
2676
|
+
this.drawAddUndoBitmap();
|
|
2677
|
+
this.refreshDrawing(false);
|
|
2678
|
+
}; // drawFloodFill()
|
|
2679
|
+
|
|
2292
2680
|
Niivue.prototype.drawPenFilled = function () {
|
|
2293
2681
|
let nPts = this.drawPenFillPts.length;
|
|
2294
2682
|
if (nPts < 2) {
|
|
@@ -2415,17 +2803,20 @@ Niivue.prototype.drawPenFilled = function () {
|
|
|
2415
2803
|
} // x column
|
|
2416
2804
|
} //y row
|
|
2417
2805
|
} //not axial
|
|
2806
|
+
//this.drawUndoBitmaps[this.currentDrawUndoBitmap]
|
|
2418
2807
|
if (
|
|
2419
2808
|
!this.drawFillOverwrites &&
|
|
2420
|
-
this.
|
|
2809
|
+
this.drawUndoBitmaps[this.currentDrawUndoBitmap].length > 0
|
|
2421
2810
|
) {
|
|
2422
|
-
let nv = this.
|
|
2811
|
+
let nv = this.drawBitmap.length;
|
|
2812
|
+
let bmp = decodeRLE(this.drawUndoBitmaps[this.currentDrawUndoBitmap], nv);
|
|
2423
2813
|
for (let i = 0; i < nv; i++) {
|
|
2424
|
-
if (
|
|
2425
|
-
this.drawBitmap[i] =
|
|
2814
|
+
if (bmp[i] === 0) continue;
|
|
2815
|
+
this.drawBitmap[i] = bmp[i];
|
|
2426
2816
|
}
|
|
2427
2817
|
}
|
|
2428
2818
|
this.drawPenFillPts = [];
|
|
2819
|
+
this.drawAddUndoBitmap();
|
|
2429
2820
|
this.refreshDrawing(false);
|
|
2430
2821
|
}; // drawPenFilled()
|
|
2431
2822
|
|
|
@@ -3767,11 +4158,14 @@ Niivue.prototype.mouseClick = function (x, y, posChange = 0, isDelta = true) {
|
|
|
3767
4158
|
}
|
|
3768
4159
|
if (this.opts.drawingEnabled) {
|
|
3769
4160
|
let pt = this.frac2vox(this.scene.crosshairPos);
|
|
4161
|
+
if (this.opts.penValue < 0 || Object.is(this.opts.penValue, -0)) {
|
|
4162
|
+
this.drawFloodFill(pt, Math.abs(this.opts.penValue));
|
|
4163
|
+
return;
|
|
4164
|
+
}
|
|
3770
4165
|
if (isNaN(this.drawPenLocation[0])) {
|
|
3771
4166
|
this.drawPenAxCorSag = axCorSag;
|
|
3772
4167
|
this.drawPenFillPts = [];
|
|
3773
4168
|
this.drawPt(...pt, this.opts.penValue);
|
|
3774
|
-
this.drawUndoBitmap = this.drawBitmap.slice();
|
|
3775
4169
|
} else {
|
|
3776
4170
|
if (
|
|
3777
4171
|
pt[0] === this.drawPenLocation[0] &&
|
|
@@ -3783,6 +4177,7 @@ Niivue.prototype.mouseClick = function (x, y, posChange = 0, isDelta = true) {
|
|
|
3783
4177
|
}
|
|
3784
4178
|
this.drawPenLocation = pt;
|
|
3785
4179
|
if (this.opts.isFilledPen) this.drawPenFillPts.push(pt);
|
|
4180
|
+
//xxxxxx
|
|
3786
4181
|
this.refreshDrawing(false);
|
|
3787
4182
|
}
|
|
3788
4183
|
this.drawScene();
|
|
@@ -4001,12 +4396,22 @@ Niivue.prototype.setInterpolation = function (isNearest) {
|
|
|
4001
4396
|
this.drawScene();
|
|
4002
4397
|
}; // setInterpolation()
|
|
4003
4398
|
|
|
4004
|
-
Niivue.prototype.calculateMvpMatrixX = function () {
|
|
4005
|
-
let
|
|
4399
|
+
Niivue.prototype.calculateMvpMatrixX = function (leftTopWidthHeight) {
|
|
4400
|
+
let gl = this.gl;
|
|
4401
|
+
gl.viewport(
|
|
4402
|
+
leftTopWidthHeight[0],
|
|
4403
|
+
leftTopWidthHeight[1],
|
|
4404
|
+
leftTopWidthHeight[2],
|
|
4405
|
+
leftTopWidthHeight[3]
|
|
4406
|
+
);
|
|
4407
|
+
let whratio = leftTopWidthHeight[2] / leftTopWidthHeight[3];
|
|
4006
4408
|
//pivot from center of objects
|
|
4007
4409
|
//let scale = this.furthestVertexFromOrigin;
|
|
4008
4410
|
//let origin = [0,0,0];
|
|
4009
|
-
|
|
4411
|
+
console.log(">>", this.back.dimsRAS, this.back.matRAS);
|
|
4412
|
+
|
|
4413
|
+
let scale = this.furthestFromPivot * 0.5;
|
|
4414
|
+
|
|
4010
4415
|
let origin = [0, 0, 0];
|
|
4011
4416
|
let projectionMatrix = mat.mat4.create();
|
|
4012
4417
|
//scale = (0.8 * scale) / this.volScaleMultiplier; //2.0 WebGL viewport has range of 2.0 [-1,-1]...[1,1]
|
|
@@ -4059,7 +4464,7 @@ Niivue.prototype.calculateMvpMatrixX = function () {
|
|
|
4059
4464
|
let modelViewProjectionMatrix = mat.mat4.create();
|
|
4060
4465
|
mat.mat4.multiply(modelViewProjectionMatrix, projectionMatrix, modelMatrix);
|
|
4061
4466
|
return [modelViewProjectionMatrix, modelMatrix, normalMatrix];
|
|
4062
|
-
}; //
|
|
4467
|
+
}; // calculateMvpMatrixX
|
|
4063
4468
|
|
|
4064
4469
|
Niivue.prototype.drawMesh3DX = function (
|
|
4065
4470
|
isDepthTest = true,
|
|
@@ -4069,10 +4474,7 @@ Niivue.prototype.drawMesh3DX = function (
|
|
|
4069
4474
|
if (this.meshes.length < 1) return;
|
|
4070
4475
|
let gl = this.gl;
|
|
4071
4476
|
let m, modelMtx, normMtx;
|
|
4072
|
-
[m, modelMtx, normMtx] = this.calculateMvpMatrixX(
|
|
4073
|
-
this.volumeObject3D,
|
|
4074
|
-
leftTopWidthHeight
|
|
4075
|
-
);
|
|
4477
|
+
[m, modelMtx, normMtx] = this.calculateMvpMatrixX(leftTopWidthHeight);
|
|
4076
4478
|
|
|
4077
4479
|
gl.enable(gl.DEPTH_TEST);
|
|
4078
4480
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
@@ -4137,6 +4539,7 @@ Niivue.prototype.drawMesh3DX = function (
|
|
|
4137
4539
|
gl.enable(gl.BLEND);
|
|
4138
4540
|
gl.depthFunc(gl.ALWAYS);
|
|
4139
4541
|
this.readyForSync = true;
|
|
4542
|
+
//gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
|
4140
4543
|
}; //drawMesh3D()
|
|
4141
4544
|
|
|
4142
4545
|
// not included in public docs
|
|
@@ -4145,7 +4548,16 @@ Niivue.prototype.draw2D = function (leftTopWidthHeight, axCorSag) {
|
|
|
4145
4548
|
let gl = this.gl;
|
|
4146
4549
|
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
|
4147
4550
|
gl.disable(gl.DEPTH_TEST);
|
|
4148
|
-
|
|
4551
|
+
/* issue56
|
|
4552
|
+
if (axCorSag === this.sliceTypeAxial) {
|
|
4553
|
+
gl.clearDepth(0.0);
|
|
4554
|
+
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
4555
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
4556
|
+
|
|
4557
|
+
this.drawMesh3DX(true,1.0,leftTopWidthHeight);
|
|
4558
|
+
this.setPivot3D();
|
|
4559
|
+
return;
|
|
4560
|
+
}*/
|
|
4149
4561
|
var crossXYZ = [
|
|
4150
4562
|
this.scene.crosshairPos[0],
|
|
4151
4563
|
this.scene.crosshairPos[1],
|
|
@@ -4267,9 +4679,8 @@ Niivue.prototype.draw2D = function (leftTopWidthHeight, axCorSag) {
|
|
|
4267
4679
|
],
|
|
4268
4680
|
"S"
|
|
4269
4681
|
);
|
|
4682
|
+
|
|
4270
4683
|
if (axCorSag !== this.sliceTypeAxial) return;
|
|
4271
|
-
// gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
|
4272
|
-
this.setPivot3D();
|
|
4273
4684
|
gl.viewport(
|
|
4274
4685
|
leftTopWidthHeight[0],
|
|
4275
4686
|
leftTopWidthHeight[1],
|
|
@@ -4279,6 +4690,7 @@ Niivue.prototype.draw2D = function (leftTopWidthHeight, axCorSag) {
|
|
|
4279
4690
|
gl.enable(gl.DEPTH_TEST);
|
|
4280
4691
|
gl.depthFunc(gl.ALWAYS);
|
|
4281
4692
|
gl.clearDepth(0.0);
|
|
4693
|
+
|
|
4282
4694
|
this.sync();
|
|
4283
4695
|
}; // draw2D()
|
|
4284
4696
|
|