From f5e0e29eb754732bc79dbf9e278a0d2dcb70acb6 Mon Sep 17 00:00:00 2001 From: Trevor Manz Date: Sat, 29 Jun 2024 00:38:18 -0400 Subject: [PATCH] refactor: Introduce Disposer util --- js/lib.ts | 20 ++++++++++++++++++++ js/mesh.ts | 24 ++++++++++++++---------- js/types.ts | 1 + js/volume.ts | 16 +++++++++++----- js/widget.ts | 19 ++++++++----------- src/ipyniivue/_widget.py | 1 + 6 files changed, 55 insertions(+), 26 deletions(-) diff --git a/js/lib.ts b/js/lib.ts index 9b7db00..93d8ba2 100644 --- a/js/lib.ts +++ b/js/lib.ts @@ -1,3 +1,4 @@ +import * as nv from "@niivue/niivue"; import type { AnyModel } from "@anywidget/types"; import type { Model } from "./types.ts"; @@ -50,3 +51,22 @@ export function determine_update_type( } return "unknown"; } + +/** + * A class to keep track of disposers for callbacks for updating the scene. + */ +export class Disposer { + #disposers = new Map void>(); + register(obj: nv.NVMesh | nv.NVImage, disposer: () => void): void { + const prefix = obj instanceof nv.NVMesh ? "mesh" : "image"; + this.#disposers.set(`${prefix}:${obj.name}`, disposer); + } + disposeAll(kind?: "mesh" | "image"): void { + for (const [name, dispose] of this.#disposers) { + if (!kind || name.startsWith(kind)) { + dispose(); + this.#disposers.delete(name); + } + } + } +} diff --git a/js/mesh.ts b/js/mesh.ts index d080a8e..1cfa10d 100644 --- a/js/mesh.ts +++ b/js/mesh.ts @@ -1,5 +1,5 @@ import * as niivue from "@niivue/niivue"; -import { determine_update_type, gather_models, unique_id } from "./lib.ts"; +import * as lib from "./lib.ts"; import type { MeshModel, Model } from "./types.ts"; /** @@ -63,29 +63,33 @@ function create_mesh( export async function render_meshes( nv: niivue.Niivue, model: Model, - cleanups: Map void>, + disposer: lib.Disposer, ) { - const mmodels = await gather_models(model, model.get("_meshes")); + const mmodels = await lib.gather_models( + model, + model.get("_meshes"), + ); const curr_names = nv.meshes.map((m) => m.name); - const new_names = mmodels.map(unique_id); - const update_type = determine_update_type(curr_names, new_names); + const new_names = mmodels.map(lib.unique_id); + const update_type = lib.determine_update_type(curr_names, new_names); if (update_type === "add") { // We know that the new meshes are the same as the old meshes, // except for the last one. We can just add the last mesh. const mmodel = mmodels[mmodels.length - 1]; const [mesh, cleanup] = create_mesh(nv, mmodel); - cleanups.set(mesh.name, cleanup); + disposer.register(mesh, cleanup); nv.addMesh(mesh); return; } - // HERE can be the place to add more update types - for (const [_, cleanup] of cleanups) cleanup(); - cleanups.clear(); + + // If we can't determine the update type, we need + // to remove all the meshes + disposer.disposeAll("mesh"); // create each mesh and add one-by-one for (const mmodel of mmodels) { const [mesh, cleanup] = create_mesh(nv, mmodel); - cleanups.set(mesh.name, cleanup); + disposer.register(mesh, cleanup); nv.addMesh(mesh); } } diff --git a/js/types.ts b/js/types.ts index 0220a1e..f2f4021 100644 --- a/js/types.ts +++ b/js/types.ts @@ -9,6 +9,7 @@ export type VolumeModel = { model_id: string } & AnyModel<{ path: File; colormap: string; opacity: number; + visible: boolean; colorbar_visible: boolean; cal_min?: number; cal_max?: number; diff --git a/js/volume.ts b/js/volume.ts index 0da00a2..4eb482b 100644 --- a/js/volume.ts +++ b/js/volume.ts @@ -21,6 +21,7 @@ function create_volume( undefined, // trustMinCalMinMax undefined, // percentileFrac undefined, // ignoreZeroVoxels + vmodel.get("visible"), // visible undefined, // useQFormNotSForm undefined, // colormapNegative undefined, // frame4D @@ -50,11 +51,16 @@ function create_volume( volume.opacity = vmodel.get("opacity"); nv.updateGLVolume(); } + function visible_changed() { + volume.visible = vmodel.get("visible"); + nv.updateGLVolume(); + } vmodel.on("change:colorbar_visible", colorbar_visible_changed); vmodel.on("change:cal_min", cal_min_changed); vmodel.on("change:cal_max", cal_max_changed); vmodel.on("change:colormap", colormap_changed); vmodel.on("change:opacity", opacity_changed); + vmodel.on("change:visible", visible_changed); return [ volume, () => { @@ -63,6 +69,7 @@ function create_volume( vmodel.off("change:cal_max", cal_max_changed); vmodel.off("change:colormap", colormap_changed); vmodel.off("change:opacity", opacity_changed); + vmodel.off("change:visible", visible_changed); }, ]; } @@ -70,7 +77,7 @@ function create_volume( export async function render_volumes( nv: niivue.Niivue, model: Model, - cleanups: Map void>, + disposer: lib.Disposer, ) { const vmodels = await lib.gather_models( model, @@ -84,7 +91,7 @@ export async function render_volumes( // except for the last one. We can just add the last volume. const vmodel = vmodels[vmodels.length - 1]; const [volume, cleanup] = create_volume(nv, vmodel); - cleanups.set(volume.id, cleanup); + disposer.register(volume, cleanup); nv.addVolume(volume); return; } @@ -95,13 +102,12 @@ export async function render_volumes( // and add the new ones. // clear all volumes - for (const [_, cleanup] of cleanups) cleanup(); - cleanups.clear(); + disposer.disposeAll("image"); // create each volume and add one-by-one for (const vmodel of vmodels) { const [volume, cleanup] = create_volume(nv, vmodel); - cleanups.set(volume.id, cleanup); + disposer.register(volume, cleanup); nv.addVolume(volume); } } diff --git a/js/widget.ts b/js/widget.ts index f768336..bc38d85 100644 --- a/js/widget.ts +++ b/js/widget.ts @@ -3,9 +3,12 @@ import type { Model } from "./types.ts"; import { render_meshes } from "./mesh.ts"; import { render_volumes } from "./volume.ts"; +import { Disposer } from "./lib.ts"; + export default { async render({ model, el }: { model: Model; el: HTMLElement }) { + const disposer = new Disposer(); const canvas = document.createElement("canvas"); const container = document.createElement("div"); container.style.height = `${model.get("height")}px`; @@ -15,13 +18,10 @@ export default { const nv = new niivue.Niivue(model.get("_opts") ?? {}); nv.attachToCanvas(canvas); - const vcleanups = new Map void>(); - await render_volumes(nv, model, vcleanups); - model.on("change:_volumes", () => render_volumes(nv, model, vcleanups)); - - const mcleanups = new Map void>(); - await render_meshes(nv, model, mcleanups); - model.on("change:_meshes", () => render_meshes(nv, model, mcleanups)); + await render_volumes(nv, model, disposer); + model.on("change:_volumes", () => render_volumes(nv, model, disposer)); + await render_meshes(nv, model, disposer); + model.on("change:_meshes", () => render_meshes(nv, model, disposer)); // Any time we change the options, we need to update the nv object // and redraw the scene. @@ -36,10 +36,7 @@ export default { // All the logic for cleaning up the event listeners and the nv object return () => { - for (const [_, cleanup] of vcleanups) cleanup(); - vcleanups.clear(); - for (const [_, cleanup] of mcleanups) cleanup(); - mcleanups.clear(); + disposer.disposeAll(); model.off("change:_volumes"); model.off("change:_opts"); }; diff --git a/src/ipyniivue/_widget.py b/src/ipyniivue/_widget.py index 7c3253a..9c19d91 100644 --- a/src/ipyniivue/_widget.py +++ b/src/ipyniivue/_widget.py @@ -30,6 +30,7 @@ class Volume(ipywidgets.Widget): path = t.Union([t.Instance(pathlib.Path), t.Unicode()]).tag( sync=True, to_json=file_serializer ) + visible = t.Bool(True).tag(sync=True) opacity = t.Float(1.0).tag(sync=True) colormap = t.Unicode("gray").tag(sync=True) colorbar_visible = t.Bool(True).tag(sync=True)