Skip to content

Commit 979a72d

Browse files
Add VolumeObject3DData trait + _volume_object_3d_data
- for use in frac2mm and mm2frac calculations - also, organized some code into js/types.ts - also, moved updateGLVolume() call for options + graph updates to be more direct instead of automatic (to prevent unnecessary extra calls)
1 parent 4d3f709 commit 979a72d

File tree

6 files changed

+175
-36
lines changed

6 files changed

+175
-36
lines changed

js/lib.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { AnyModel, TypedBufferPayload } from "./types.ts";
2+
import type { NVConfigOptions } from "@niivue/niivue";
23

34
function delay(ms: number) {
45
return new Promise((resolve) => setTimeout(resolve, ms));
@@ -14,6 +15,33 @@ function dataViewToBase64(dataView: DataView) {
1415
return btoa(binaryString);
1516
}
1617

18+
export function deserializeOptions(
19+
options: Partial<Record<keyof NVConfigOptions, unknown>>,
20+
): NVConfigOptions {
21+
const result: Partial<NVConfigOptions> = {};
22+
const specialValues: Record<string, number> = {
23+
Infinity: Number.POSITIVE_INFINITY,
24+
"-Infinity": Number.NEGATIVE_INFINITY,
25+
NaN: Number.NaN,
26+
"-0": -0,
27+
};
28+
29+
for (const [key, value] of Object.entries(options) as [
30+
keyof NVConfigOptions,
31+
unknown,
32+
][]) {
33+
if (typeof value === "string" && value in specialValues) {
34+
// biome-ignore lint/suspicious/noExplicitAny: NVConfigOptions
35+
(result as any)[key] = specialValues[value];
36+
} else {
37+
// biome-ignore lint/suspicious/noExplicitAny: NVConfigOptions
38+
(result as any)[key] = value;
39+
}
40+
}
41+
42+
return result as NVConfigOptions;
43+
}
44+
1745
export function handleBufferMsg(
1846
// biome-ignore lint/suspicious/noExplicitAny: targetObject can be any
1947
targetObject: any,

js/types.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ export interface AnyModel<T extends object = object> extends BaseAnyModel<T> {
1111
onChange: (value: Partial<T>) => void;
1212
}
1313

14+
// just part of the NiivueObject3D in niivue
15+
export type NiivueObject3D = {
16+
id: number;
17+
extents_min: number[];
18+
extents_max: number[];
19+
scale: number[];
20+
furthest_vertex_from_origin?: number;
21+
field_of_view_de_oblique_mm?: number[];
22+
};
23+
1424
interface FileInput {
1525
name: string;
1626
data: DataView;
@@ -94,7 +104,7 @@ export type VolumeModel = AnyModel<{
94104
modulation_image: number | null;
95105
modulate_alpha: number;
96106

97-
hdr: Partial<NIFTI1>;
107+
hdr: Partial<NIFTI1>; // only updated via frontend...but this might change in the future..
98108
img: DataView;
99109
dims: number[];
100110
}>;
@@ -166,6 +176,8 @@ export type Model = AnyModel<{
166176
scene: Scene;
167177
overlay_outline_width: number;
168178
overlay_alpha_shader: number;
179+
180+
_volume_object_3d_data: NiivueObject3D; // only updated via frontend (1-way comm)
169181
}>;
170182

171183
// Custom message datas
@@ -214,6 +226,7 @@ export type CustomMessagePayload =
214226
| { type: "set_gamma"; data: SetGammaData }
215227
| { type: "resize_listener"; data: [] }
216228
| { type: "draw_scene"; data: [] }
229+
| { type: "update_gl_volume"; data: [] }
217230
| {
218231
type: "set_volume_render_illumination";
219232
data: SetVolumeRenderIlluminationData;

js/widget.ts

Lines changed: 33 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,39 +10,13 @@ import type {
1010
CustomMessagePayload,
1111
MeshModel,
1212
Model,
13+
NiivueObject3D,
1314
Scene,
1415
VolumeModel,
1516
} from "./types.ts";
1617

1718
let nv: niivue.Niivue;
1819

19-
function deserializeOptions(
20-
options: Partial<Record<keyof niivue.NVConfigOptions, unknown>>,
21-
): niivue.NVConfigOptions {
22-
const result: Partial<niivue.NVConfigOptions> = {};
23-
const specialValues: Record<string, number> = {
24-
Infinity: Number.POSITIVE_INFINITY,
25-
"-Infinity": Number.NEGATIVE_INFINITY,
26-
NaN: Number.NaN,
27-
"-0": -0,
28-
};
29-
30-
for (const [key, value] of Object.entries(options) as [
31-
keyof niivue.NVConfigOptions,
32-
unknown,
33-
][]) {
34-
if (typeof value === "string" && value in specialValues) {
35-
// biome-ignore lint/suspicious/noExplicitAny: NVConfigOptions
36-
(result as any)[key] = specialValues[value];
37-
} else {
38-
// biome-ignore lint/suspicious/noExplicitAny: NVConfigOptions
39-
(result as any)[key] = value;
40-
}
41-
}
42-
43-
return result as niivue.NVConfigOptions;
44-
}
45-
4620
// Attach model event handlers
4721
function attachModelEventHandlers(
4822
nv: niivue.Niivue,
@@ -60,13 +34,13 @@ function attachModelEventHandlers(
6034
}
6135
});
6236

63-
// Any time we change the options, we need to update the nv gl
37+
// Any time the backend changes the options, we need to update the nv gl
38+
// but...need to filter out changes that are *actually* from the frontend
39+
// so, we don't call updateGLVolume here (instead, it's explicitly called from the backend)
6440
model.on("change:opts", () => {
6541
const serializedOpts = model.get("opts");
66-
const opts = deserializeOptions(serializedOpts);
67-
42+
const opts = lib.deserializeOptions(serializedOpts);
6843
nv.document.opts = { ...nv.opts, ...opts };
69-
nv.updateGLVolume();
7044
});
7145

7246
// Other nv prop changes
@@ -111,9 +85,6 @@ function attachModelEventHandlers(
11185
// biome-ignore lint/suspicious/noExplicitAny: Update graph vals, only clear out old vals when needed
11286
(nv.graph as any)[key] = value;
11387
}
114-
if (nv._gl) {
115-
nv.updateGLVolume();
116-
}
11788
}
11889

11990
function scene_changed() {
@@ -214,6 +185,10 @@ function attachModelEventHandlers(
214185
nv.drawScene();
215186
break;
216187
}
188+
case "update_gl_volume": {
189+
nv.updateGLVolume();
190+
break;
191+
}
217192
case "set_volume_render_illumination": {
218193
if (nv._gl) {
219194
let [gradientAmount] = data;
@@ -333,6 +308,29 @@ function attachNiivueEventHandlers(nv: niivue.Niivue, model: Model) {
333308
},
334309
});
335310

311+
const originalRefreshLayersMethod = nv.refreshLayers;
312+
nv.refreshLayers = new Proxy(originalRefreshLayersMethod, {
313+
apply: (target, thisArg, argumentsList) => {
314+
Reflect.apply(target, thisArg, argumentsList);
315+
316+
if (nv.volumeObject3D) {
317+
const currentVolumeObject3D: NiivueObject3D = {
318+
id: nv.volumeObject3D.id,
319+
extents_min: nv.volumeObject3D.extentsMin,
320+
extents_max: nv.volumeObject3D.extentsMax,
321+
scale: nv.volumeObject3D.scale,
322+
furthest_vertex_from_origin:
323+
nv.volumeObject3D.furthestVertexFromOrigin,
324+
field_of_view_de_oblique_mm: nv.volumeObject3D
325+
.fieldOfViewDeObliqueMM as number[],
326+
};
327+
model.set("_volume_object_3d_data", currentVolumeObject3D);
328+
model.save_changes();
329+
console.log("_volume_object_3d_data set");
330+
}
331+
},
332+
});
333+
336334
nv.onImageLoaded = async (volume: niivue.NVImage) => {
337335
// Check if the volume is already in the backend
338336
const volumeID = volume.id;
@@ -652,7 +650,7 @@ export default {
652650
if (!nv) {
653651
console.log("Creating new Niivue instance");
654652
const serializedOpts = model.get("opts") ?? {};
655-
const opts = deserializeOptions(serializedOpts);
653+
const opts = lib.deserializeOptions(serializedOpts);
656654
nv = new niivue.Niivue(opts);
657655
}
658656

src/ipyniivue/serializers.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
Graph,
2222
NIFTI1Hdr,
2323
Scene,
24+
VolumeObject3DData,
2425
)
2526
from .utils import is_negative_zero
2627

@@ -355,3 +356,40 @@ def serialize_enum(instance: enum.Enum, widget: object):
355356
if isinstance(instance, enum.Enum):
356357
return instance.value
357358
return instance
359+
360+
361+
def serialize_to_none(instance: object, widget: object):
362+
"""
363+
Serialize to None.
364+
365+
Parameters
366+
----------
367+
instance : object
368+
The object to serialize.
369+
widget : object
370+
The NiiVue widget the instance is a part of.
371+
372+
Returns
373+
-------
374+
None
375+
"""
376+
return None
377+
378+
379+
def deserialize_volume_object_3d_data(instance: dict, widget: object):
380+
"""
381+
Deserialize serialized VolumeObject3DData.
382+
383+
Parameters
384+
----------
385+
instance : dict
386+
The dictionary from the frontend.
387+
widget : object
388+
The NiiVue widget the instance is a part of.
389+
390+
Returns
391+
-------
392+
VolumeObject3DData
393+
The deserialized VolumeObject3DData instance.
394+
"""
395+
return VolumeObject3DData(**instance)

src/ipyniivue/traits.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,58 @@ def _propagate_parent_change(self, change):
450450
self._parent._notify_scene_changed()
451451

452452

453+
class VolumeObject3DData(t.HasTraits):
454+
"""
455+
Represents data from a 3D volume object, partial of niivue's NiivueObject3D.
456+
457+
Properties
458+
----------
459+
id : int
460+
Unique identifier for the object.
461+
extents_min : list of float
462+
Minimum extents of the object in each dimension.
463+
extents_max : list of float
464+
Maximum extents of the object in each dimension.
465+
scale : list of float
466+
Scale factors for each dimension.
467+
furthest_vertex_from_origin : float or None
468+
Distance to the furthest vertex from the origin.
469+
field_of_view_de_oblique_mm : list of float or None
470+
Field of view in de-oblique millimeters.
471+
"""
472+
473+
id = t.Int().tag(sync=False)
474+
extents_min = t.List(t.Float()).tag(sync=False)
475+
extents_max = t.List(t.Float()).tag(sync=False)
476+
scale = t.List(t.Float()).tag(sync=False)
477+
furthest_vertex_from_origin = t.Float(allow_none=True, default_value=None).tag(
478+
sync=False
479+
)
480+
field_of_view_de_oblique_mm = t.List(
481+
t.Float(), allow_none=True, default_value=None
482+
).tag(sync=False)
483+
484+
@t.validate(
485+
"id",
486+
"extents_min",
487+
"extents_max",
488+
"scale",
489+
"furthest_vertex_from_origin",
490+
"field_of_view_de_oblique_mm",
491+
)
492+
def _validate_no_change(self, proposal):
493+
trait_name = proposal["trait"].name
494+
if (
495+
trait_name in self._trait_values
496+
and (
497+
self._trait_values[trait_name] or self._trait_values[trait_name] == 0.0
498+
)
499+
and self._trait_values[trait_name] != proposal["value"]
500+
):
501+
raise t.TraitError(f"Cannot modify '{trait_name}' once set.")
502+
return proposal["value"]
503+
504+
453505
CAMEL_TO_SNAKE_SCENE = {
454506
"renderAzimuth": "render_azimuth",
455507
"renderElevation": "render_elevation",

src/ipyniivue/widget.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
deserialize_hdr,
3434
deserialize_options,
3535
deserialize_scene,
36+
deserialize_volume_object_3d_data,
3637
serialize_colormap_label,
3738
serialize_enum,
3839
serialize_file,
@@ -41,13 +42,15 @@
4142
serialize_ndarray,
4243
serialize_options,
4344
serialize_scene,
45+
serialize_to_none,
4446
)
4547
from .traits import (
4648
LUT,
4749
ColorMap,
4850
Graph,
4951
NIFTI1Hdr,
5052
Scene,
53+
VolumeObject3DData,
5154
)
5255
from .utils import (
5356
ChunkedDataHandler,
@@ -682,6 +685,11 @@ class NiiVue(BaseAnyWidget):
682685
)
683686

684687
_canvas_attached = t.Bool(False).tag(sync=True)
688+
_volume_object_3d_data = t.Instance(VolumeObject3DData, allow_none=True).tag(
689+
sync=True,
690+
to_json=serialize_to_none,
691+
from_json=deserialize_volume_object_3d_data,
692+
)
685693

686694
# other props
687695
background_masks_overlays = t.Int(0).tag(sync=True)
@@ -758,6 +766,7 @@ def _notify_opts_changed(self):
758766
"type": "change",
759767
}
760768
)
769+
self.send({"type": "update_gl_volume", "data": []})
761770

762771
def _notify_graph_changed(self):
763772
self.notify_change(
@@ -769,6 +778,7 @@ def _notify_graph_changed(self):
769778
"type": "change",
770779
}
771780
)
781+
self.send({"type": "update_gl_volume", "data": []})
772782

773783
def _notify_scene_changed(self):
774784
self.notify_change(

0 commit comments

Comments
 (0)