diff --git a/js/types.ts b/js/types.ts new file mode 100644 index 0000000..5acdce3 --- /dev/null +++ b/js/types.ts @@ -0,0 +1,12 @@ +import type { AnyModel } from "@anywidget/types"; + +export type VolumeModel = { model_id: string } & AnyModel<{ + path: { name: string; data: DataView }; + colormap: string; + opacity: number; +}>; + +export type Model = AnyModel<{ + _volumes: string[]; + _opts: Record; +}> diff --git a/js/widget.css b/js/widget.css deleted file mode 100644 index 1ad618f..0000000 --- a/js/widget.css +++ /dev/null @@ -1 +0,0 @@ -/* widget.css */ \ No newline at end of file diff --git a/js/widget.js b/js/widget.js new file mode 100644 index 0000000..f7951ab --- /dev/null +++ b/js/widget.js @@ -0,0 +1,169 @@ +// @ts-check +import { Niivue, NVImage } from "@niivue/niivue"; + +/** + * Generates a unique file name for a volume (using the model id and the volume path) + * + * We need to keep track of the volumes from Python somehow, and the model_id is unique + * to the volume sent from Python. This function generates a new filename for the volume + * using the existing filename and model + * + * @param {import('./types').VolumeModel} model + * @returns {string} + */ +function volume_id(model) { + let path = model.get("path"); + // take the first 6 characters of the model_id, it should be unique enough + let id = model.model_id.slice(0, 6); + return id + ":" + path.name; +} + +/** + * Determine what type of update is necessary to go from `old_arr` to `new_arr`. + * + * If cannot determine the update type, return "unknown". Only "add" is supported + * for now. + * + * @template T + * @param {Array} old_arr + * @param {Array} new_arr + * @returns {"add" | "unknown"} + */ +function determine_update_type(old_arr, new_arr) { + if ( + old_arr.length === (new_arr.length - 1) && + old_arr.every((v, i) => new_arr[i] === v) + ) { + return "add"; + } + return "unknown"; +} + +/** + * @param {import('./types').Model} model + * @param {string[]} ids + * @returns {Promise>} + */ +function gather_models(model, ids) { + /** @type {Array>} */ + let models = []; + let widget_manager = model.widget_manager; + for (let id of ids) { + let model_id = id.slice("IPY_MODEL_".length); + models.push(widget_manager.get_model(model_id)); + } + return Promise.all(models); +} + +/** + * @param {import('./types').Model} model + */ +function gather_volume_models(model) { + let ids = model.get("_volumes"); + return gather_models(model, ids); +} + +/** + * Create a new NVImage and attach the necessary event listeners + * Returns the NVImage and a cleanup function that removes the event listeners. + * + * @param {Niivue} nv + * @param {import('./types').VolumeModel} vmodel + * @returns {[NVImage, () => void]} + */ +function create_volume(nv, vmodel) { + let volume = new NVImage( + vmodel.get("path").data.buffer, + volume_id(vmodel), + vmodel.get("colormap"), + vmodel.get("opacity"), + ); + function colormap_changed() { + nv.setColormap(volume.id, vmodel.get("colormap")); + } + function opacity_changed() { + let idx = nv.volumes.findIndex(v => v === volume); + nv.setOpacity(idx, vmodel.get("opacity")); + } + vmodel.on("change:colormap", colormap_changed); + vmodel.on("change:opacity", opacity_changed); + return [volume, () => { + vmodel.off("change:colormap", colormap_changed); + vmodel.off("change:opacity", opacity_changed); + }] +} + + +/** + * @param {Niivue} nv + * @param {import("./types").Model} model + * @param {Map void>} cleanups + */ +async function render_volumes(nv, model, cleanups) { + let vmodels = await gather_volume_models(model); + let curr_names = nv.volumes.map(v => v.name); + let new_names = vmodels.map(volume_id); + let update_type = determine_update_type(curr_names, new_names); + if (update_type === "add") { + // We know that the new volumes are the same as the old volumes, + // except for the last one. We can just add the last volume. + let vmodel = vmodels[vmodels.length - 1]; + let [volume, cleanup] = create_volume(nv, vmodel); + cleanups.set(volume.id, cleanup); + nv.addVolume(volume); + return; + } + // HERE can be the place to add more update types + // ... + + // We don't know what the update type is, so we need to remove all volumes + // and add the new ones. + + // clear all volumes + for (let [_, cleanup] of cleanups) cleanup(); + cleanups.clear(); + + // create each volume and add one-by-one + for (let vmodel of vmodels) { + let [volume, cleanup] = create_volume(nv, vmodel); + cleanups.set(volume.id, cleanup); + nv.addVolume(volume); + } +} + +export default { + /** @param {{ model: import("./types").Model, el: HTMLElement }} ctx */ + render({ model, el }) { + + let canvas = document.createElement("canvas"); + let container = document.createElement("div"); + container.style.height = "300px"; + container.appendChild(canvas); + el.appendChild(container); + + let nv = new Niivue(model.get("_opts") ?? {}); + nv.attachToCanvas(canvas); + + /** @type {Map void>} */ + let vcleanups = new Map(); + render_volumes(nv, model, vcleanups); + // Any time we change the volumes, we need to update the nv object + model.on("change:_volumes", () => render_volumes(nv, model, vcleanups)); + + // Any time we change the options, we need to update the nv object + // and redraw the scene. + model.on("change:_opts", () => { + nv.opts = { ...nv.opts, ...model.get("_opts") }; + nv.drawScene(); + nv.updateGLVolume(); + }); + + // All the logic for cleaning up the event listeners and the nv object + return () => { + for (let [_, cleanup] of vcleanups) cleanup(); + vcleanups.clear(); + model.off("change:_volumes"); + model.off("change:_opts"); + } + } +} diff --git a/js/widget_send.js b/js/widget_send.js deleted file mode 100644 index efc8e78..0000000 --- a/js/widget_send.js +++ /dev/null @@ -1,24 +0,0 @@ -import "./widget.css"; - -import { Niivue } from "@niivue/niivue"; - -async function render({ model, el }) { - console.log("This approach is deprecated!") - let canvas = document.createElement("canvas"); - let container = document.createElement("div"); - container.style.height = "300px"; - container.appendChild(canvas); - el.appendChild(container); - let nv = new Niivue(); - nv.attachToCanvas(canvas); - - model.on("msg:custom", (msg) => { - if (msg.type === "api") { - console.log("API message received!"); - let funcname = msg.func; - nv[funcname](...msg.args); - } - }); -} - -export default { render }; diff --git a/js/widget_traitlet.js b/js/widget_traitlet.js deleted file mode 100644 index 55ded97..0000000 --- a/js/widget_traitlet.js +++ /dev/null @@ -1,72 +0,0 @@ -import "./widget.css"; - -import { Niivue, NVImage, SLICE_TYPE } from "@niivue/niivue"; - -async function render({ model, el }) { - const options = { dragAndDropEnabled: false }; - let canvas = document.createElement("canvas"); - let container = document.createElement("div"); - container.style.height = "300px"; - container.appendChild(canvas); - el.appendChild(container); - let nv = new Niivue(options); - nv.attachToCanvas(canvas); - - console.log("volume"); - console.log(nv.volumes); // this will be [] - - function render_volumes() { - let new_volumes = model.get("_volumes"); - console.log(new_volumes); - new_volumes.forEach(async (volume_file) => { - let image = new NVImage(volume_file.data.buffer, volume_file.name); - await nv.addVolume(image); - }); - } - - render_volumes(); // initial render - // model.on("change:_volumes", render_volumes); //later render - - // let image = new NVImage(volume_file.data.buffer, volume_file.name); - // await nv.addVolume(image); - - // let volume_file = model.get("volume_file"); - // let image = new NVImage(volume_file.data.buffer, volume_file.name); - // await nv.addVolume(image); - - // model.on("change:volume_file", async() => { - // let volume_file = model.get("volume_file"); - // let image = new NVImage(volume_file.data.buffer, volume_file.name); - // await nv.addVolume(image); - // }); - - // model.on("change:opacity", () => { - // let value = model.get("opacity"); - // nv.setOpacity(0, value); - // }); - - // model.on("change:colormap", () => { - // let value = model.get("colormap"); - // nv.setColorMap(nv.volumes[0].id, value); - // }); - - // model.on("change:slice_type", () => { - // let value = model.get("slice_type"); - // nv.setSliceType(value); - // }); - - // model.on("change:drag_mode", () => { - // let value = model.get("drag_mode"); - // if (value == "DRAG_MODES.CONTRAST") { - // nv.opts.dragMode = nv.dragModes.contrast; - // } - // if (value == "DRAG_MODES.MEASUREMENT") { - // nv.opts.dragMode = nv.dragModes.measurement; - // } - // if (value == "DRAG_MODES.PAN") { - // nv.opts.dragMode = nv.dragModes.pan; - // } - // }); -} - -export default { render }; diff --git a/js/widget_vscode.js b/js/widget_vscode.js deleted file mode 100644 index 3b68c33..0000000 --- a/js/widget_vscode.js +++ /dev/null @@ -1,55 +0,0 @@ -import "./widget.css"; - -import { Niivue } from "@niivue/niivue"; - -async function render({ model, el }) { - let canvas = document.createElement("canvas"); - let container = document.createElement("div"); - container.style.height = "300px"; - container.appendChild(canvas); - el.appendChild(container); - let nv = new Niivue(); - let value = model.get("volume"); - nv.attachToCanvas(canvas); - if (value != "") { - nv.loadVolumes(value); - } - - model.on("change:volume", async () => { - value = model.get("volume"); - console.log("volume changed"); - console.log(value); - await nv.loadVolumes(value); - }); - - model.on("change:opacity", () => { - let value = model.get("opacity"); - nv.setOpacity(0, value); - }); - - model.on("change:colormap", () => { - let value = model.get("colormap"); - nv.setColorMap(nv.volumes[0].id, value); - }); - - model.on("change:slice_type", () => { - let value = model.get("slice_type"); - nv.setSliceType(value); - }); - - model.on("change:drag_mode", () => { - let value = model.get("drag_mode"); - if (value == "DRAG_MODES.CONTRAST") { - nv.opts.dragMode = nv.dragModes.contrast; - } - if (value == "DRAG_MODES.MEASUREMENT") { - nv.opts.dragMode = nv.dragModes.measurement; - } - if (value == "DRAG_MODES.PAN") { - nv.opts.dragMode = nv.dragModes.pan; - } - }); - -} - -export default { render }; diff --git a/package-lock.json b/package-lock.json index e98ed24..aac8898 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,12 +5,21 @@ "packages": { "": { "dependencies": { + "@anywidget/types": "^0.1.5", "@niivue/niivue": "^0.39.0" }, "devDependencies": { "esbuild": "^0.19.11" } }, + "node_modules/@anywidget/types": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@anywidget/types/-/types-0.1.5.tgz", + "integrity": "sha512-WuZrR/g+HyZ0/tYZSsQfdEdz39EF8khBkERAqQUy7U15LyTUWu5wkNnFB9NKySK+9OQS7y3vyTL6MbwqytZdOg==", + "dependencies": { + "@jupyter-widgets/base": "^6.0.6" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.19.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", @@ -379,6 +388,105 @@ "node": ">=12" } }, + "node_modules/@jupyter-widgets/base": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@jupyter-widgets/base/-/base-6.0.7.tgz", + "integrity": "sha512-a4VoUtL+90mGH6VE6m78D2J9aNy9Q1JrPP91HAQTXBysVpCLTtq0Ie8EupN5Su7V8eFwl/wz91J2m0p4jY4h0g==", + "dependencies": { + "@jupyterlab/services": "^6.0.0 || ^7.0.0", + "@lumino/coreutils": "^1.11.1 || ^2.1", + "@lumino/messaging": "^1.10.1 || ^2.1", + "@lumino/widgets": "^1.30.0 || ^2.1", + "@types/backbone": "1.4.14", + "@types/lodash": "^4.14.134", + "backbone": "1.4.0", + "jquery": "^3.1.1", + "lodash": "^4.17.4" + } + }, + "node_modules/@jupyter/ydoc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@jupyter/ydoc/-/ydoc-1.1.1.tgz", + "integrity": "sha512-fXx9CbUwUlXBsJo83tBQL3T0MgWT4YYz2ozcSFj0ymZSohAnI1uo7N9CPpVe4/nmc9uG1lFdlXC4XQBevi2jSA==", + "dependencies": { + "@jupyterlab/nbformat": "^3.0.0 || ^4.0.0-alpha.21 || ^4.0.0", + "@lumino/coreutils": "^1.11.0 || ^2.0.0", + "@lumino/disposable": "^1.10.0 || ^2.0.0", + "@lumino/signaling": "^1.10.0 || ^2.0.0", + "y-protocols": "^1.0.5", + "yjs": "^13.5.40" + } + }, + "node_modules/@jupyterlab/coreutils": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-6.1.2.tgz", + "integrity": "sha512-YzuKhlviq6/uIazjb2+G9vKPemFfof8z0D0nUnN99aU6oIH40UhtImJf6wvTbKruRmeCferg6AWlKjXxCup6lQ==", + "dependencies": { + "@lumino/coreutils": "^2.1.2", + "@lumino/disposable": "^2.1.2", + "@lumino/signaling": "^2.1.2", + "minimist": "~1.2.0", + "path-browserify": "^1.0.0", + "url-parse": "~1.5.4" + } + }, + "node_modules/@jupyterlab/nbformat": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@jupyterlab/nbformat/-/nbformat-4.1.2.tgz", + "integrity": "sha512-geEau0hCQV85JmsQDpjhcmvA7Sl0XfQ1yfZzz+HuolJI83OJYba2nhsaw8JB2Fa/oT0kXBiO90PE/ka2Lsk8+A==", + "dependencies": { + "@lumino/coreutils": "^2.1.2" + } + }, + "node_modules/@jupyterlab/services": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@jupyterlab/services/-/services-7.1.2.tgz", + "integrity": "sha512-DFgoc2GN0z20T/cwl7D1xBk3BwkojbsyHXHGv+TKQQmZLEf+tusWiepiUlAvsNDMNkVZpS8rD8gaj0CzCdKsFw==", + "dependencies": { + "@jupyter/ydoc": "^1.1.1", + "@jupyterlab/coreutils": "^6.1.2", + "@jupyterlab/nbformat": "^4.1.2", + "@jupyterlab/settingregistry": "^4.1.2", + "@jupyterlab/statedb": "^4.1.2", + "@lumino/coreutils": "^2.1.2", + "@lumino/disposable": "^2.1.2", + "@lumino/polling": "^2.1.2", + "@lumino/properties": "^2.0.1", + "@lumino/signaling": "^2.1.2", + "ws": "^8.11.0" + } + }, + "node_modules/@jupyterlab/settingregistry": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@jupyterlab/settingregistry/-/settingregistry-4.1.2.tgz", + "integrity": "sha512-v0lBXo7zV+O9GpuY44RMkJz5rD8PeG/me0HP+UzD6gOaYEOPzdMgkY0n02hY0DDWCe47GLBlHuPi7nOZCqGTMg==", + "dependencies": { + "@jupyterlab/nbformat": "^4.1.2", + "@jupyterlab/statedb": "^4.1.2", + "@lumino/commands": "^2.2.0", + "@lumino/coreutils": "^2.1.2", + "@lumino/disposable": "^2.1.2", + "@lumino/signaling": "^2.1.2", + "@rjsf/utils": "^5.13.4", + "ajv": "^8.12.0", + "json5": "^2.2.3" + }, + "peerDependencies": { + "react": ">=16" + } + }, + "node_modules/@jupyterlab/statedb": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@jupyterlab/statedb/-/statedb-4.1.2.tgz", + "integrity": "sha512-WXdtOxrtoRMkVmvrxsgu+3VjEmm1Gd4CcbGezTnFMTnfk7vvjkE81XeYfaFnAwzgmaHEJYYfqKzEOpZ9bUFxqg==", + "dependencies": { + "@lumino/commands": "^2.2.0", + "@lumino/coreutils": "^2.1.2", + "@lumino/disposable": "^2.1.2", + "@lumino/properties": "^2.0.1", + "@lumino/signaling": "^2.1.2" + } + }, "node_modules/@lukeed/csprng": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", @@ -398,6 +506,151 @@ "node": ">=8" } }, + "node_modules/@lumino/algorithm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@lumino/algorithm/-/algorithm-2.0.1.tgz", + "integrity": "sha512-iA+uuvA7DeNFB0/cQpIWNgO1c6z4pOSigifjstLy+rxf1U5ZzxIq+xudnEuTbWgKSTviG02j4cKwCyx1PO6rzA==" + }, + "node_modules/@lumino/collections": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@lumino/collections/-/collections-1.9.3.tgz", + "integrity": "sha512-2i2Wf1xnfTgEgdyKEpqM16bcYRIhUOGCDzaVCEZACVG9R1CgYwOe3zfn71slBQOVSjjRgwYrgLXu4MBpt6YK+g==", + "dependencies": { + "@lumino/algorithm": "^1.9.2" + } + }, + "node_modules/@lumino/collections/node_modules/@lumino/algorithm": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@lumino/algorithm/-/algorithm-1.9.2.tgz", + "integrity": "sha512-Z06lp/yuhz8CtIir3PNTGnuk7909eXt4ukJsCzChsGuot2l5Fbs96RJ/FOHgwCedaX74CtxPjXHXoszFbUA+4A==" + }, + "node_modules/@lumino/commands": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@lumino/commands/-/commands-2.2.0.tgz", + "integrity": "sha512-xm+4rFithAd/DLZheQcS0GJaI3m0gVg07mCEZAWBLolN5e7w6XTr17VuD7J6KSjdBygMKZ3n8GlEkpcRNWEajA==", + "dependencies": { + "@lumino/algorithm": "^2.0.1", + "@lumino/coreutils": "^2.1.2", + "@lumino/disposable": "^2.1.2", + "@lumino/domutils": "^2.0.1", + "@lumino/keyboard": "^2.0.1", + "@lumino/signaling": "^2.1.2", + "@lumino/virtualdom": "^2.0.1" + } + }, + "node_modules/@lumino/coreutils": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lumino/coreutils/-/coreutils-2.1.2.tgz", + "integrity": "sha512-vyz7WzchTO4HQ8iVAxvSUmb5o/8t3cz1vBo8V4ZIaPGada0Jx0xe3tKQ8bXp4pjHc+AEhMnkCnlUyVYMWbnj4A==" + }, + "node_modules/@lumino/disposable": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lumino/disposable/-/disposable-2.1.2.tgz", + "integrity": "sha512-0qmB6zPt9+uj4SVMTfISn0wUOjYHahtKotwxDD5flfcscj2gsXaFCXO4Oqot1zcsZbg8uJmTUhEzAvFW0QhFNA==", + "dependencies": { + "@lumino/signaling": "^2.1.2" + } + }, + "node_modules/@lumino/domutils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@lumino/domutils/-/domutils-2.0.1.tgz", + "integrity": "sha512-tbcfhsdKH04AMjSgYAYGD2xE80YcjrqKnfMTeU2NHt4J294Hzxs1GvEmSMk5qJ3Bbgwx6Z4BbQ7apnFg8Gc6cA==" + }, + "node_modules/@lumino/dragdrop": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@lumino/dragdrop/-/dragdrop-2.1.4.tgz", + "integrity": "sha512-/ckaYPHIZC1Ff0pU2H3WDI/Xm7V3i0XnyYG4PeZvG1+ovc0I0zeZtlb6qZXne0Vi2r8L2a0624FjF2CwwgNSnA==", + "dependencies": { + "@lumino/coreutils": "^2.1.2", + "@lumino/disposable": "^2.1.2" + } + }, + "node_modules/@lumino/keyboard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@lumino/keyboard/-/keyboard-2.0.1.tgz", + "integrity": "sha512-R2mrH9HCEcv/0MSAl7bEUbjCNOnhrg49nXZBEVckg//TEG+sdayCsyrbJNMPcZ07asIPKc6mq3v7DpAmDKqh+w==" + }, + "node_modules/@lumino/messaging": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@lumino/messaging/-/messaging-1.10.3.tgz", + "integrity": "sha512-F/KOwMCdqvdEG8CYAJcBSadzp6aI7a47Fr60zAKGqZATSRRRV41q53iXU7HjFPqQqQIvdn9Z7J32rBEAyQAzww==", + "dependencies": { + "@lumino/algorithm": "^1.9.2", + "@lumino/collections": "^1.9.3" + } + }, + "node_modules/@lumino/messaging/node_modules/@lumino/algorithm": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@lumino/algorithm/-/algorithm-1.9.2.tgz", + "integrity": "sha512-Z06lp/yuhz8CtIir3PNTGnuk7909eXt4ukJsCzChsGuot2l5Fbs96RJ/FOHgwCedaX74CtxPjXHXoszFbUA+4A==" + }, + "node_modules/@lumino/polling": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lumino/polling/-/polling-2.1.2.tgz", + "integrity": "sha512-hv6MT7xuSrw2gW4VIoiz3L366ZdZz4oefht+7HIW/VUB6seSDp0kVyZ4P9P4I4s/LauuzPqru3eWr7QAsFZyGA==", + "dependencies": { + "@lumino/coreutils": "^2.1.2", + "@lumino/disposable": "^2.1.2", + "@lumino/signaling": "^2.1.2" + } + }, + "node_modules/@lumino/properties": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@lumino/properties/-/properties-2.0.1.tgz", + "integrity": "sha512-RPtHrp8cQqMnTC915lOIdrmsbPDCC7PhPOZb2YY7/Jj6dEdwmGhoMthc2tBEYWoHP+tU/hVm8UR/mEQby22srQ==" + }, + "node_modules/@lumino/signaling": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lumino/signaling/-/signaling-2.1.2.tgz", + "integrity": "sha512-KtwKxx+xXkLOX/BdSqtvnsqBTPKDIENFBKeYkMTxstQc3fHRmyTzmaVoeZES+pr1EUy3e8vM4pQFVQpb8VsDdA==", + "dependencies": { + "@lumino/algorithm": "^2.0.1", + "@lumino/coreutils": "^2.1.2" + } + }, + "node_modules/@lumino/virtualdom": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@lumino/virtualdom/-/virtualdom-2.0.1.tgz", + "integrity": "sha512-WNM+uUZX7vORhlDRN9NmhEE04Tz1plDjtbwsX+i/51pQj2N2r7+gsVPY/gR4w+I5apmC3zG8/BojjJYIwi8ogA==", + "dependencies": { + "@lumino/algorithm": "^2.0.1" + } + }, + "node_modules/@lumino/widgets": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@lumino/widgets/-/widgets-2.3.1.tgz", + "integrity": "sha512-t3yKoXY4P1K1Tiv7ABZLKjwtn2gFIbaK0jnjFhoHNlzX5q43cm7FjtCFQWrvJbBN6Heq9qq00JPOWXeZ3IlQdg==", + "dependencies": { + "@lumino/algorithm": "^2.0.1", + "@lumino/commands": "^2.2.0", + "@lumino/coreutils": "^2.1.2", + "@lumino/disposable": "^2.1.2", + "@lumino/domutils": "^2.0.1", + "@lumino/dragdrop": "^2.1.4", + "@lumino/keyboard": "^2.0.1", + "@lumino/messaging": "^2.0.1", + "@lumino/properties": "^2.0.1", + "@lumino/signaling": "^2.1.2", + "@lumino/virtualdom": "^2.0.1" + } + }, + "node_modules/@lumino/widgets/node_modules/@lumino/collections": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@lumino/collections/-/collections-2.0.1.tgz", + "integrity": "sha512-8TbAU/48XVPKc/FOhGHLuugf2Gmx6vhVEx867KGG5fLwDOI8EW4gTno78yJUk8G0QpgNa+sdpB/LwbJFNIratg==", + "dependencies": { + "@lumino/algorithm": "^2.0.1" + } + }, + "node_modules/@lumino/widgets/node_modules/@lumino/messaging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@lumino/messaging/-/messaging-2.0.1.tgz", + "integrity": "sha512-Z1b9Sq7i2yw7BN/u9ezoBUMYK06CsQXO7BqpczSnEO0PfwFf9dWi7y9VcIySOBz9uogsT1uczZMIMtLefk+xPQ==", + "dependencies": { + "@lumino/algorithm": "^2.0.1", + "@lumino/collections": "^2.0.1" + } + }, "node_modules/@niivue/niivue": { "version": "0.39.0", "resolved": "https://registry.npmjs.org/@niivue/niivue/-/niivue-0.39.0.tgz", @@ -416,6 +669,24 @@ "@rollup/rollup-linux-x64-gnu": "^4.6.1" } }, + "node_modules/@rjsf/utils": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.17.1.tgz", + "integrity": "sha512-q1Igz/cuM2hi+jiXFkoaXqdRTUFB+a0jfVKNmZlHmvPmfYeeJfcfyOTzO8dQ41fHNHUFb15ryxa/TblDQimwkA==", + "dependencies": { + "json-schema-merge-allof": "^0.8.1", + "jsonpointer": "^5.0.1", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "react-is": "^18.2.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.14.0 || >=17" + } + }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.9.6", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz", @@ -428,6 +699,38 @@ "linux" ] }, + "node_modules/@types/backbone": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@types/backbone/-/backbone-1.4.14.tgz", + "integrity": "sha512-85ldQ99fiYTJFBlZuAJRaCdvTZKZ2p1fSs3fVf+6Ub6k1X0g0hNJ0qJ/2FOByyyAQYLtbEz3shX5taKQfBKBDw==", + "dependencies": { + "@types/jquery": "*", + "@types/underscore": "*" + } + }, + "node_modules/@types/jquery": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.29.tgz", + "integrity": "sha512-oXQQC9X9MOPRrMhPHHOsXqeQDnWeCDT3PelUIg/Oy8FAbzSZtFHRjc7IpbfFVmpLtJ+UOoywpRsuO5Jxjybyeg==", + "dependencies": { + "@types/sizzle": "*" + } + }, + "node_modules/@types/lodash": { + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==" + }, + "node_modules/@types/sizzle": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==" + }, + "node_modules/@types/underscore": { + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.15.tgz", + "integrity": "sha512-HP38xE+GuWGlbSRq9WrZkousaQ7dragtZCruBVMi0oX1migFZavZ3OROKHSkNp/9ouq82zrWtZpg18jFnVN96g==" + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -441,6 +744,21 @@ "node": ">=10" } }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/array-equal": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.2.tgz", @@ -449,6 +767,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/backbone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/backbone/-/backbone-1.4.0.tgz", + "integrity": "sha512-RLmDrRXkVdouTg38jcgHhyQ/2zjg7a8E6sz2zxfz21Hh17xDJYUHBZimVIt5fUyS8vbfpeSmTL3gUjTEvUV3qQ==", + "dependencies": { + "underscore": ">=1.8.3" + } + }, + "node_modules/compute-gcd": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/compute-gcd/-/compute-gcd-1.2.1.tgz", + "integrity": "sha512-TwMbxBNz0l71+8Sc4czv13h4kEqnchV9igQZBi6QUaz09dnz13juGnnaWWJTRsP3brxOoxeB4SA2WELLw1hCtg==", + "dependencies": { + "validate.io-array": "^1.0.3", + "validate.io-function": "^1.0.2", + "validate.io-integer-array": "^1.0.0" + } + }, + "node_modules/compute-lcm": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/compute-lcm/-/compute-lcm-1.1.2.tgz", + "integrity": "sha512-OFNPdQAXnQhDSKioX8/XYT6sdUlXwpeMjfd6ApxMJfyZ4GxmLR1xvMERctlYhlHwIiz6CSpBc2+qYKjHGZw4TQ==", + "dependencies": { + "compute-gcd": "^1.2.1", + "validate.io-array": "^1.0.3", + "validate.io-function": "^1.0.2", + "validate.io-integer-array": "^1.0.0" + } + }, "node_modules/daikon": { "version": "1.2.45", "resolved": "https://registry.npmjs.org/daikon/-/daikon-1.2.45.tgz", @@ -499,6 +846,11 @@ "@esbuild/win32-x64": "0.19.12" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, "node_modules/fflate": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", @@ -509,11 +861,125 @@ "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/jpeg-lossless-decoder-js": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/jpeg-lossless-decoder-js/-/jpeg-lossless-decoder-js-2.1.0.tgz", "integrity": "sha512-jeb6656kiWT1DctSlnie0u+iHhd/9yQ3T5RgbJI7x0blx3olxGHENspeRZQHPKFeSanSTsLRRptmfEN8qiiYWA==" }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "peer": true + }, + "node_modules/json-schema-compare": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/json-schema-compare/-/json-schema-compare-0.2.2.tgz", + "integrity": "sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==", + "dependencies": { + "lodash": "^4.17.4" + } + }, + "node_modules/json-schema-merge-allof": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/json-schema-merge-allof/-/json-schema-merge-allof-0.8.1.tgz", + "integrity": "sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w==", + "dependencies": { + "compute-lcm": "^1.1.2", + "json-schema-compare": "^0.2.2", + "lodash": "^4.17.20" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lib0": { + "version": "0.2.89", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.89.tgz", + "integrity": "sha512-5j19vcCjsQhvLG6mcDD+nprtJUCbmqLz5Hzt5xgi9SV6RIW/Dty7ZkVZHGBuPOADMKjQuKDvuQTH495wsmw8DQ==", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/nifti-reader-js": { "version": "0.6.8", "resolved": "https://registry.npmjs.org/nifti-reader-js/-/nifti-reader-js-0.6.8.tgz", @@ -527,6 +993,54 @@ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, "node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -540,6 +1054,80 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/validate.io-array": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/validate.io-array/-/validate.io-array-1.0.6.tgz", + "integrity": "sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg==" + }, + "node_modules/validate.io-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/validate.io-function/-/validate.io-function-1.0.2.tgz", + "integrity": "sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ==" + }, + "node_modules/validate.io-integer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/validate.io-integer/-/validate.io-integer-1.0.5.tgz", + "integrity": "sha512-22izsYSLojN/P6bppBqhgUDjCkr5RY2jd+N2a3DCAUey8ydvrZ/OkGvFPR7qfOpwR2LC5p4Ngzxz36g5Vgr/hQ==", + "dependencies": { + "validate.io-number": "^1.0.3" + } + }, + "node_modules/validate.io-integer-array": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-integer-array/-/validate.io-integer-array-1.0.0.tgz", + "integrity": "sha512-mTrMk/1ytQHtCY0oNO3dztafHYyGU88KL+jRxWuzfOmQb+4qqnWmI+gykvGp8usKZOM0H7keJHEbRaFiYA0VrA==", + "dependencies": { + "validate.io-array": "^1.0.3", + "validate.io-integer": "^1.0.4" + } + }, + "node_modules/validate.io-number": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/validate.io-number/-/validate.io-number-1.0.3.tgz", + "integrity": "sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg==" + }, + "node_modules/ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xss": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/xss/-/xss-0.0.9.tgz", @@ -547,6 +1135,41 @@ "engines": { "node": ">= 0.6.0" } + }, + "node_modules/y-protocols": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz", + "integrity": "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==", + "dependencies": { + "lib0": "^0.2.85" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, + "node_modules/yjs": { + "version": "13.6.12", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.12.tgz", + "integrity": "sha512-KOT8ILoyVH2f/PxPadeu5kVVS055D1r3x1iFfJVJzFdnN98pVGM8H07NcKsO+fG3F7/0tf30Vnokf5YIqhU/iw==", + "dependencies": { + "lib0": "^0.2.86" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } } } } diff --git a/package.json b/package.json index 4b92487..9ab0d48 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "build": "esbuild js/*.js --minify --format=esm --bundle --outdir=src/ipyniivue_experimental/static" }, "dependencies": { + "@anywidget/types": "^0.1.5", "@niivue/niivue": "^0.39.0" }, "devDependencies": { diff --git a/scripts/generate_options_mixin.py b/scripts/generate_options_mixin.py new file mode 100644 index 0000000..6cf64fc --- /dev/null +++ b/scripts/generate_options_mixin.py @@ -0,0 +1,142 @@ +import pathlib +import typing + +from ipyniivue_experimental._constants import ( + DragMode, + MuliplanarType, + SliceType, +) + + +def camel_to_snake(name: str): + return "".join(["_" + c.lower() if c.isupper() else c for c in name]).lstrip("_") + + +def type_hint(value: typing.Any): + if isinstance(value, bool): + return "bool" + elif isinstance(value, int): + return "int" + elif isinstance(value, float): + return "float" + elif isinstance(value, str): + return "str" + elif isinstance(value, tuple): + return "tuple" + elif isinstance(value, list): + return "list" + elif isinstance(value, dict): + return "dict" + elif isinstance(value, SliceType): + return "SliceType" + elif isinstance(value, MuliplanarType): + return "MuliplanarType" + elif isinstance(value, DragMode): + return "DragMode" + else: + return "typing.Any" + + +def get_value(value: typing.Any): + if isinstance(value, SliceType): + return f"SliceType.{value.name}" + if isinstance(value, MuliplanarType): + return f"MuliplanarType.{value.name}" + if isinstance(value, DragMode): + return f"DragMode.{value.name}" + return repr(value) + + +def generate_mixin(options): + lines = [ + "# This file is automatically generated by scripts/generate_options_mixin.py", + "# Do not edit this file directly", + "from __future__ import annotations", + "import typing", + "", + "from ._constants import SliceType, MuliplanarType, DragMode", + "", + '__all__ = ["OptionsMixin"]', + "", + "class OptionsMixin:", + ] + for option, value in options.items(): + snake_name = camel_to_snake(option) + hint = type_hint(value) + lines.append(" @property") + lines.append(f" def {snake_name}(self) -> {hint}:") + lines.append(f" return self._opts.get('{option}', {get_value(value)})") + lines.append("") + lines.append(f" @{snake_name}.setter") + lines.append(f" def {snake_name}(self, value: {hint}):") + lines.append(f' self._opts = {{ **self._opts, "{option}": value }}') + lines.append("") + return "\n".join(lines) + + +if __name__ == "__main__": + # Copied from niivue (should be able to automatically generate this) + DEFAULT_OPTIONS = dict( + textHeight=0.06, + colorbarHeight=0.05, + crosshairWidth=1, + rulerWidth=4, + show3Dcrosshair=False, + backColor=(0, 0, 0, 1), + crosshairColor=(1, 0, 0, 1), + fontColor=(0.5, 0.5, 0.5, 1), + selectionBoxColor=(1, 1, 1, 0.5), + clipPlaneColor=(0.7, 0, 0.7, 0.5), + rulerColor=(1, 0, 0, 0.8), + colorbarMargin=0.05, + trustCalMinMax=True, + clipPlaneHotKey="KeyC", + viewModeHotKey="KeyV", + doubleTouchTimeout=500, + longTouchTimeout=1000, + keyDebounceTime=50, + isNearestInterpolation=False, + isResizeCanvas=True, + isAtlasOutline=False, + isRuler=False, + isColorbar=False, + isOrientCube=False, + multiplanarPadPixels=0, + multiplanarForceRender=False, + isRadiologicalConvention=False, + meshThicknessOn2D=float("inf"), + dragMode=DragMode.CONTRAST, + yoke3Dto2DZoom=False, + isDepthPickMesh=False, + isCornerOrientationText=False, + sagittalNoseLeft=False, + isSliceMM=False, + isV1SliceShader=False, + isHighResolutionCapable=True, + logLevel="info", + loadingText="waiting for images...", + isForceMouseClickToVoxelCenters=False, + dragAndDropEnabled=True, + drawingEnabled=False, + penValue=1, + floodFillNeighbors=6, + isFilledPen=False, + thumbnail="", + maxDrawUndoBitmaps=8, + sliceType=SliceType.MULTIPLANAR, + meshXRay=0.0, + isAntiAlias=None, + limitFrames4D=float("nan"), + isAdditiveBlend=False, + showLegend=True, + legendBackgroundColor=(0.3, 0.3, 0.3, 0.5), + legendTextColor=(1.0, 1.0, 1.0, 1.0), + multiplanarLayout=MuliplanarType.AUTO, + renderOverlayBlend=1.0, + ) + code = generate_mixin(DEFAULT_OPTIONS) + loc = ( + pathlib.Path(__file__).parent + / "../src/ipyniivue_experimental/_options_mixin.py" + ) + loc.write_text(code) diff --git a/src/ipyniivue_experimental/__init__.py b/src/ipyniivue_experimental/__init__.py index e748f66..ba09a4f 100644 --- a/src/ipyniivue_experimental/__init__.py +++ b/src/ipyniivue_experimental/__init__.py @@ -1,99 +1,9 @@ import importlib.metadata -import pathlib - -import anywidget -import traitlets +from ._constants import SliceType, DragMode, MuliplanarType # noqa +from ._widget import AnyNiivue # noqa try: __version__ = importlib.metadata.version("ipyniivue_experimental") except importlib.metadata.PackageNotFoundError: __version__ = "unknown" - - -from enum import Enum - - -class SLICE_TYPE(Enum): - AXIAL = 1 - CORONAL = 2 - SAGITTAL = 3 - MULTIPLANAR = 4 - RENDER = 5 - - -class DRAG_MODES(Enum): - CONTRAST = 1 - MEASUREMENT = 2 - PAN = 3 - - -def file_serializer(instance: pathlib.Path, widget): - if instance == None: - return None - return {"name": instance.name, "data": instance.read_bytes()} - - -class AnyNiivue(anywidget.AnyWidget): - path_root = pathlib.Path.cwd() - _esm = ( - path_root / "src" / "ipyniivue_experimental" / "static" / "widget_traitlet.js" - ) - - _volumes = traitlets.List( - trait=traitlets.Instance(pathlib.Path), default_value=[], allow_none=True - ).tag(sync=True, to_json=lambda obj, _: [file_serializer(item, _) for item in obj]) - - def add_volume(self, new_volume): - self._volumes = self._volumes + [new_volume] - print(self._volumes) - - opacity = traitlets.Float(1.0).tag(sync=True) - - def set_opacity(self, value): - self.opacity = value - - def get_opacitiy(self): - return self.opacity - - colormap = traitlets.Unicode("").tag(sync=True) - - def set_colormap(self, value): - self.colormap = value - - def get_colormap(self): - return self.colormap - - slice_type = traitlets.Integer(0).tag(sync=True) - - def set_slice_type(self, my_slice_type): - self.slice_type = int(my_slice_type.value) - - drag_mode = traitlets.Unicode("").tag(sync=True) - - def set_drag_mode(self, value): - self.drag_mode = str(value) - - - -class AnyNiivueOldSend(anywidget.AnyWidget): - path_root = pathlib.Path(__file__).parent / "static" - _esm = path_root / "widget_send.js" - - def load_volumes(self, volume_list): - self.send({"type": "api", "func": "loadVolumes", "args": [volume_list]}) - - def load_meshes(self, mesh_list): - self.send({"type": "api", "func": "loadMeshes", "args": [mesh_list]}) - - def set_opacity(self, opacity=1): - self.send({"type": "api", "func": "setOpacity", "args": [[0], [opacity]]}) - - def set_crosshair_color(self, color): - self.send({"type": "api", "func": "setCrosshairColor", "args": [color]}) - - def set_crosshair_color(self, color): - self.send({"type": "api", "func": "setCrosshairColor", "args": [color]}) - - def set_crosshair_width(self, width): - self.send({"type": "api", "func": "setCrosshairWidth", "args": [width]}) diff --git a/src/ipyniivue_experimental/_constants.py b/src/ipyniivue_experimental/_constants.py new file mode 100644 index 0000000..750f779 --- /dev/null +++ b/src/ipyniivue_experimental/_constants.py @@ -0,0 +1,27 @@ +import enum + +__all__ = [ + "SliceType", + "DragMode", + "MuliplanarType", +] + +class SliceType(enum.Enum): + AXIAL = 1 + CORONAL = 2 + SAGITTAL = 3 + MULTIPLANAR = 4 + RENDER = 5 + + +class DragMode(enum.Enum): + CONTRAST = 1 + MEASUREMENT = 2 + PAN = 3 + + +class MuliplanarType(enum.Enum): + AUTO = 0 + COLUMN = 1 + GRID = 2 + ROW = 3 diff --git a/src/ipyniivue_experimental/_options_mixin.py b/src/ipyniivue_experimental/_options_mixin.py new file mode 100644 index 0000000..b7afe2f --- /dev/null +++ b/src/ipyniivue_experimental/_options_mixin.py @@ -0,0 +1,457 @@ +# This file is automatically generated by scripts/generate_options_mixin.py +# Do not edit this file directly +from __future__ import annotations +import typing + +from ._constants import SliceType, MuliplanarType, DragMode + +__all__ = ["OptionsMixin"] + +class OptionsMixin: + @property + def text_height(self) -> float: + return self._opts.get('textHeight', 0.06) + + @text_height.setter + def text_height(self, value: float): + self._opts = { **self._opts, "textHeight": value } + + @property + def colorbar_height(self) -> float: + return self._opts.get('colorbarHeight', 0.05) + + @colorbar_height.setter + def colorbar_height(self, value: float): + self._opts = { **self._opts, "colorbarHeight": value } + + @property + def crosshair_width(self) -> int: + return self._opts.get('crosshairWidth', 1) + + @crosshair_width.setter + def crosshair_width(self, value: int): + self._opts = { **self._opts, "crosshairWidth": value } + + @property + def ruler_width(self) -> int: + return self._opts.get('rulerWidth', 4) + + @ruler_width.setter + def ruler_width(self, value: int): + self._opts = { **self._opts, "rulerWidth": value } + + @property + def show3_dcrosshair(self) -> bool: + return self._opts.get('show3Dcrosshair', False) + + @show3_dcrosshair.setter + def show3_dcrosshair(self, value: bool): + self._opts = { **self._opts, "show3Dcrosshair": value } + + @property + def back_color(self) -> tuple: + return self._opts.get('backColor', (0, 0, 0, 1)) + + @back_color.setter + def back_color(self, value: tuple): + self._opts = { **self._opts, "backColor": value } + + @property + def crosshair_color(self) -> tuple: + return self._opts.get('crosshairColor', (1, 0, 0, 1)) + + @crosshair_color.setter + def crosshair_color(self, value: tuple): + self._opts = { **self._opts, "crosshairColor": value } + + @property + def font_color(self) -> tuple: + return self._opts.get('fontColor', (0.5, 0.5, 0.5, 1)) + + @font_color.setter + def font_color(self, value: tuple): + self._opts = { **self._opts, "fontColor": value } + + @property + def selection_box_color(self) -> tuple: + return self._opts.get('selectionBoxColor', (1, 1, 1, 0.5)) + + @selection_box_color.setter + def selection_box_color(self, value: tuple): + self._opts = { **self._opts, "selectionBoxColor": value } + + @property + def clip_plane_color(self) -> tuple: + return self._opts.get('clipPlaneColor', (0.7, 0, 0.7, 0.5)) + + @clip_plane_color.setter + def clip_plane_color(self, value: tuple): + self._opts = { **self._opts, "clipPlaneColor": value } + + @property + def ruler_color(self) -> tuple: + return self._opts.get('rulerColor', (1, 0, 0, 0.8)) + + @ruler_color.setter + def ruler_color(self, value: tuple): + self._opts = { **self._opts, "rulerColor": value } + + @property + def colorbar_margin(self) -> float: + return self._opts.get('colorbarMargin', 0.05) + + @colorbar_margin.setter + def colorbar_margin(self, value: float): + self._opts = { **self._opts, "colorbarMargin": value } + + @property + def trust_cal_min_max(self) -> bool: + return self._opts.get('trustCalMinMax', True) + + @trust_cal_min_max.setter + def trust_cal_min_max(self, value: bool): + self._opts = { **self._opts, "trustCalMinMax": value } + + @property + def clip_plane_hot_key(self) -> str: + return self._opts.get('clipPlaneHotKey', 'KeyC') + + @clip_plane_hot_key.setter + def clip_plane_hot_key(self, value: str): + self._opts = { **self._opts, "clipPlaneHotKey": value } + + @property + def view_mode_hot_key(self) -> str: + return self._opts.get('viewModeHotKey', 'KeyV') + + @view_mode_hot_key.setter + def view_mode_hot_key(self, value: str): + self._opts = { **self._opts, "viewModeHotKey": value } + + @property + def double_touch_timeout(self) -> int: + return self._opts.get('doubleTouchTimeout', 500) + + @double_touch_timeout.setter + def double_touch_timeout(self, value: int): + self._opts = { **self._opts, "doubleTouchTimeout": value } + + @property + def long_touch_timeout(self) -> int: + return self._opts.get('longTouchTimeout', 1000) + + @long_touch_timeout.setter + def long_touch_timeout(self, value: int): + self._opts = { **self._opts, "longTouchTimeout": value } + + @property + def key_debounce_time(self) -> int: + return self._opts.get('keyDebounceTime', 50) + + @key_debounce_time.setter + def key_debounce_time(self, value: int): + self._opts = { **self._opts, "keyDebounceTime": value } + + @property + def is_nearest_interpolation(self) -> bool: + return self._opts.get('isNearestInterpolation', False) + + @is_nearest_interpolation.setter + def is_nearest_interpolation(self, value: bool): + self._opts = { **self._opts, "isNearestInterpolation": value } + + @property + def is_resize_canvas(self) -> bool: + return self._opts.get('isResizeCanvas', True) + + @is_resize_canvas.setter + def is_resize_canvas(self, value: bool): + self._opts = { **self._opts, "isResizeCanvas": value } + + @property + def is_atlas_outline(self) -> bool: + return self._opts.get('isAtlasOutline', False) + + @is_atlas_outline.setter + def is_atlas_outline(self, value: bool): + self._opts = { **self._opts, "isAtlasOutline": value } + + @property + def is_ruler(self) -> bool: + return self._opts.get('isRuler', False) + + @is_ruler.setter + def is_ruler(self, value: bool): + self._opts = { **self._opts, "isRuler": value } + + @property + def is_colorbar(self) -> bool: + return self._opts.get('isColorbar', False) + + @is_colorbar.setter + def is_colorbar(self, value: bool): + self._opts = { **self._opts, "isColorbar": value } + + @property + def is_orient_cube(self) -> bool: + return self._opts.get('isOrientCube', False) + + @is_orient_cube.setter + def is_orient_cube(self, value: bool): + self._opts = { **self._opts, "isOrientCube": value } + + @property + def multiplanar_pad_pixels(self) -> int: + return self._opts.get('multiplanarPadPixels', 0) + + @multiplanar_pad_pixels.setter + def multiplanar_pad_pixels(self, value: int): + self._opts = { **self._opts, "multiplanarPadPixels": value } + + @property + def multiplanar_force_render(self) -> bool: + return self._opts.get('multiplanarForceRender', False) + + @multiplanar_force_render.setter + def multiplanar_force_render(self, value: bool): + self._opts = { **self._opts, "multiplanarForceRender": value } + + @property + def is_radiological_convention(self) -> bool: + return self._opts.get('isRadiologicalConvention', False) + + @is_radiological_convention.setter + def is_radiological_convention(self, value: bool): + self._opts = { **self._opts, "isRadiologicalConvention": value } + + @property + def mesh_thickness_on2_d(self) -> float: + return self._opts.get('meshThicknessOn2D', inf) + + @mesh_thickness_on2_d.setter + def mesh_thickness_on2_d(self, value: float): + self._opts = { **self._opts, "meshThicknessOn2D": value } + + @property + def drag_mode(self) -> DragMode: + return self._opts.get('dragMode', DragMode.CONTRAST) + + @drag_mode.setter + def drag_mode(self, value: DragMode): + self._opts = { **self._opts, "dragMode": value } + + @property + def yoke3_dto2_d_zoom(self) -> bool: + return self._opts.get('yoke3Dto2DZoom', False) + + @yoke3_dto2_d_zoom.setter + def yoke3_dto2_d_zoom(self, value: bool): + self._opts = { **self._opts, "yoke3Dto2DZoom": value } + + @property + def is_depth_pick_mesh(self) -> bool: + return self._opts.get('isDepthPickMesh', False) + + @is_depth_pick_mesh.setter + def is_depth_pick_mesh(self, value: bool): + self._opts = { **self._opts, "isDepthPickMesh": value } + + @property + def is_corner_orientation_text(self) -> bool: + return self._opts.get('isCornerOrientationText', False) + + @is_corner_orientation_text.setter + def is_corner_orientation_text(self, value: bool): + self._opts = { **self._opts, "isCornerOrientationText": value } + + @property + def sagittal_nose_left(self) -> bool: + return self._opts.get('sagittalNoseLeft', False) + + @sagittal_nose_left.setter + def sagittal_nose_left(self, value: bool): + self._opts = { **self._opts, "sagittalNoseLeft": value } + + @property + def is_slice_m_m(self) -> bool: + return self._opts.get('isSliceMM', False) + + @is_slice_m_m.setter + def is_slice_m_m(self, value: bool): + self._opts = { **self._opts, "isSliceMM": value } + + @property + def is_v1_slice_shader(self) -> bool: + return self._opts.get('isV1SliceShader', False) + + @is_v1_slice_shader.setter + def is_v1_slice_shader(self, value: bool): + self._opts = { **self._opts, "isV1SliceShader": value } + + @property + def is_high_resolution_capable(self) -> bool: + return self._opts.get('isHighResolutionCapable', True) + + @is_high_resolution_capable.setter + def is_high_resolution_capable(self, value: bool): + self._opts = { **self._opts, "isHighResolutionCapable": value } + + @property + def log_level(self) -> str: + return self._opts.get('logLevel', 'info') + + @log_level.setter + def log_level(self, value: str): + self._opts = { **self._opts, "logLevel": value } + + @property + def loading_text(self) -> str: + return self._opts.get('loadingText', 'waiting for images...') + + @loading_text.setter + def loading_text(self, value: str): + self._opts = { **self._opts, "loadingText": value } + + @property + def is_force_mouse_click_to_voxel_centers(self) -> bool: + return self._opts.get('isForceMouseClickToVoxelCenters', False) + + @is_force_mouse_click_to_voxel_centers.setter + def is_force_mouse_click_to_voxel_centers(self, value: bool): + self._opts = { **self._opts, "isForceMouseClickToVoxelCenters": value } + + @property + def drag_and_drop_enabled(self) -> bool: + return self._opts.get('dragAndDropEnabled', True) + + @drag_and_drop_enabled.setter + def drag_and_drop_enabled(self, value: bool): + self._opts = { **self._opts, "dragAndDropEnabled": value } + + @property + def drawing_enabled(self) -> bool: + return self._opts.get('drawingEnabled', False) + + @drawing_enabled.setter + def drawing_enabled(self, value: bool): + self._opts = { **self._opts, "drawingEnabled": value } + + @property + def pen_value(self) -> int: + return self._opts.get('penValue', 1) + + @pen_value.setter + def pen_value(self, value: int): + self._opts = { **self._opts, "penValue": value } + + @property + def flood_fill_neighbors(self) -> int: + return self._opts.get('floodFillNeighbors', 6) + + @flood_fill_neighbors.setter + def flood_fill_neighbors(self, value: int): + self._opts = { **self._opts, "floodFillNeighbors": value } + + @property + def is_filled_pen(self) -> bool: + return self._opts.get('isFilledPen', False) + + @is_filled_pen.setter + def is_filled_pen(self, value: bool): + self._opts = { **self._opts, "isFilledPen": value } + + @property + def thumbnail(self) -> str: + return self._opts.get('thumbnail', '') + + @thumbnail.setter + def thumbnail(self, value: str): + self._opts = { **self._opts, "thumbnail": value } + + @property + def max_draw_undo_bitmaps(self) -> int: + return self._opts.get('maxDrawUndoBitmaps', 8) + + @max_draw_undo_bitmaps.setter + def max_draw_undo_bitmaps(self, value: int): + self._opts = { **self._opts, "maxDrawUndoBitmaps": value } + + @property + def slice_type(self) -> SliceType: + return self._opts.get('sliceType', SliceType.MULTIPLANAR) + + @slice_type.setter + def slice_type(self, value: SliceType): + self._opts = { **self._opts, "sliceType": value } + + @property + def mesh_x_ray(self) -> float: + return self._opts.get('meshXRay', 0.0) + + @mesh_x_ray.setter + def mesh_x_ray(self, value: float): + self._opts = { **self._opts, "meshXRay": value } + + @property + def is_anti_alias(self) -> typing.Any: + return self._opts.get('isAntiAlias', None) + + @is_anti_alias.setter + def is_anti_alias(self, value: typing.Any): + self._opts = { **self._opts, "isAntiAlias": value } + + @property + def limit_frames4_d(self) -> float: + return self._opts.get('limitFrames4D', nan) + + @limit_frames4_d.setter + def limit_frames4_d(self, value: float): + self._opts = { **self._opts, "limitFrames4D": value } + + @property + def is_additive_blend(self) -> bool: + return self._opts.get('isAdditiveBlend', False) + + @is_additive_blend.setter + def is_additive_blend(self, value: bool): + self._opts = { **self._opts, "isAdditiveBlend": value } + + @property + def show_legend(self) -> bool: + return self._opts.get('showLegend', True) + + @show_legend.setter + def show_legend(self, value: bool): + self._opts = { **self._opts, "showLegend": value } + + @property + def legend_background_color(self) -> tuple: + return self._opts.get('legendBackgroundColor', (0.3, 0.3, 0.3, 0.5)) + + @legend_background_color.setter + def legend_background_color(self, value: tuple): + self._opts = { **self._opts, "legendBackgroundColor": value } + + @property + def legend_text_color(self) -> tuple: + return self._opts.get('legendTextColor', (1.0, 1.0, 1.0, 1.0)) + + @legend_text_color.setter + def legend_text_color(self, value: tuple): + self._opts = { **self._opts, "legendTextColor": value } + + @property + def multiplanar_layout(self) -> MuliplanarType: + return self._opts.get('multiplanarLayout', MuliplanarType.AUTO) + + @multiplanar_layout.setter + def multiplanar_layout(self, value: MuliplanarType): + self._opts = { **self._opts, "multiplanarLayout": value } + + @property + def render_overlay_blend(self) -> float: + return self._opts.get('renderOverlayBlend', 1.0) + + @render_overlay_blend.setter + def render_overlay_blend(self, value: float): + self._opts = { **self._opts, "renderOverlayBlend": value } diff --git a/src/ipyniivue_experimental/_utils.py b/src/ipyniivue_experimental/_utils.py new file mode 100644 index 0000000..1d49346 --- /dev/null +++ b/src/ipyniivue_experimental/_utils.py @@ -0,0 +1,20 @@ +import typing +import pathlib +import enum + + +def snake_to_camel(snake_str: str): + components = snake_str.split("_") + return components[0] + "".join(x.title() for x in components[1:]) + + +def file_serializer(instance: typing.Union[pathlib.Path, str], widget: object): + if isinstance(instance, str): + # make sure we have a pathlib.Path instance + instance = pathlib.Path(instance) + return {"name": instance.name, "data": instance.read_bytes()} + + +def serialize_options(instance: dict, widget: object): + # serialize enums as their value + return {k: v.value if isinstance(v, enum.Enum) else v for k, v in instance.items()} diff --git a/src/ipyniivue_experimental/_widget.py b/src/ipyniivue_experimental/_widget.py new file mode 100644 index 0000000..2875b54 --- /dev/null +++ b/src/ipyniivue_experimental/_widget.py @@ -0,0 +1,45 @@ +import pathlib + +import anywidget +import ipywidgets +import traitlets as t + +from ._options_mixin import OptionsMixin +from ._utils import file_serializer, serialize_options, snake_to_camel + +__all__ = ["AnyNiivue"] + + +class Volume(ipywidgets.Widget): + path = t.Union([t.Instance(pathlib.Path), t.Unicode()]).tag( + sync=True, to_json=file_serializer + ) + opacity = t.Float(1.0).tag(sync=True) + colormap = t.Unicode("gray").tag(sync=True) + + +class AnyNiivue(OptionsMixin, anywidget.AnyWidget): + _esm = pathlib.Path(__file__).parent / "static" / "widget.js" + _opts = t.Dict({}).tag(sync=True, to_json=serialize_options) + _volumes = t.List(t.Instance(Volume), default_value=[]).tag( + sync=True, **ipywidgets.widget_serialization + ) + + def __init__(self, **opts): + # convert to JS camelCase options + _opts = {snake_to_camel(k): v for k, v in opts.items()} + super().__init__(_opts=_opts, _volumes=[]) + + def load_volumes(self, volumes: list): + """Loads a list of volumes into the widget""" + volumes = [Volume(**item) for item in volumes] + self._volumes = volumes + + def add_volume(self, volume: dict): + """Adds a single volume to the widget""" + self._volumes = self._volumes + [Volume(**volume)] + + @property + def volumes(self): + """Returns the list of volumes""" + return self._volumes