Skip to content

Commit edb49dd

Browse files
authored
Merge pull request #827 from Kitware/tooltips
Hover over tool fill shows tooltips and Sparse state file
2 parents 634631b + 0cc85d6 commit edb49dd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1927
-1486
lines changed

.github/workflows/e2e.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
name: E2E Testing on ${{ matrix.os }}
1111
runs-on: ${{ matrix.os }}
1212
env:
13-
DOWNLOAD_TIMEOUT: 220000
13+
DOWNLOAD_TIMEOUT: 60000
1414
VITE_SHOW_SAMPLE_DATA: true
1515
steps:
1616
- uses: actions/checkout@v4

docs/loading_data.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,7 @@ To layer images:
6767
1. Under the Rendering tab, an opacity slider changes the transparency of the upper layer.
6868

6969
![Add Layer](./assets/add-layer.jpg)
70+
71+
## State Files
72+
73+
Load preconfigured scenes with annotations, segment groups, and view settings via [state files](./state_files.md). State files can embed data (`*.volview.zip`) or reference remote data via URIs (`*.volview.json`).

docs/state_files.md

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,69 @@
11
# State Files
22

3-
VolView state files are a great way to save your scene and data to either be used later, or for distributing to collaborators and other users. These files store all of the information you need to restore the state of VolView: your data, annotations, camera positions, background colors, colormaps, multi-view layouts, and more.
3+
VolView state files save your scene configuration: annotations, camera positions, colormaps, layouts, and more. There are two formats:
44

5-
State files can be saved by clicking on "Disk" icon in the top of the toolbar. This button will generate a `*.volview.zip` file that can then be re-opened in VolView at any time.
5+
## Zip State Files (`*.volview.zip`)
66

7-
When saving VolView state, your data is saved along with the application state. This way, when you send a state file to a collaborator, they too can open the state file and load the previously saved data. However, this means that your state file will be as large as your dataset(s) and may contain patient identifying information. Please follow your institutes HIPAA, IRB and other regulatory and confidentiality requirements.
7+
Save by clicking the "Disk" icon in the toolbar. This embeds your image data that was loaded from local files alongside the application state. Useful for sharing annotations with collaborators.
88

9-
State files are loaded by clicking on the "Folder" icon immediately below the save-state Disk icon. This will bring up a file browser for you to select and load your state file.
9+
## Sparse Manifest Files (`*.volview.json`)
1010

11-
TIP: State files are a great way for developers to transfer data into / out of VolView for integration with other systems. For example, they can be used to integrate VolView with access control systems, to streamline workflows, or to ingest results from AI systems.
11+
JSON files that reference remote data via URIs instead of embedding it. Useful for:
12+
13+
- Linking to data hosted on servers
14+
- Sharing annotations without duplicating large datasets
15+
- Integrating with external systems (AI pipelines, access control, etc.)
16+
17+
Example manifest:
18+
19+
```json
20+
{
21+
"version": "6.2.0",
22+
"dataSources": [
23+
{ "id": 0, "type": "uri", "uri": "https://example.com/scan.zip" },
24+
{ "id": 1, "type": "uri", "uri": "https://example.com/segmentation.nii.gz" }
25+
],
26+
"segmentGroups": [
27+
{
28+
"id": "seg-1",
29+
"dataSourceId": 1,
30+
"metadata": {
31+
"name": "Tumor Segmentation",
32+
"parentImage": "0",
33+
"segments": {
34+
"order": [1],
35+
"byValue": {
36+
"1": { "value": 1, "name": "Tumor", "color": [255, 0, 0, 255] }
37+
}
38+
}
39+
}
40+
}
41+
],
42+
"tools": {
43+
"rectangles": {
44+
"tools": [
45+
{
46+
"imageID": "0",
47+
"frameOfReference": {
48+
"planeNormal": [0, 0, 1],
49+
"planeOrigin": [0, 0, 50]
50+
},
51+
"slice": 50,
52+
"firstPoint": [-20, -20, 50],
53+
"secondPoint": [20, 20, 50],
54+
"label": "lesion"
55+
}
56+
],
57+
"labels": {
58+
"lesion": { "color": "red" }
59+
}
60+
}
61+
}
62+
}
63+
```
64+
65+
## Loading State Files
66+
67+
- **Drag and drop** onto VolView
68+
- **File browser** via the "Folder" icon below the save button
69+
- **URL parameter**: `?urls=[https://example.com/session.volview.json]`

package-lock.json

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/EditableChipList.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ const itemsToRender = computed(() =>
3939
mandatory
4040
>
4141
<v-row dense>
42-
<v-col cols="6" v-for="({ key, title }, idx) in itemsToRender" :key="key">
42+
<v-col
43+
cols="12"
44+
v-for="({ key, title }, idx) in itemsToRender"
45+
:key="key"
46+
>
4347
<v-item v-slot="{ selectedClass, toggle }" :value="key">
4448
<v-chip
4549
variant="tonal"
@@ -59,7 +63,7 @@ const itemsToRender = computed(() =>
5963
</v-col>
6064

6165
<!-- Add Label button -->
62-
<v-col cols="6">
66+
<v-col cols="12">
6367
<v-chip variant="outlined" class="w-100" @click="$emit('create')">
6468
<v-icon class="mr-2">mdi-plus</v-icon>
6569
{{ createLabelText }}

src/components/ModulePanel.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ export default defineComponent({
149149
position: relative;
150150
flex: 2;
151151
overflow: auto;
152+
scrollbar-gutter: stable;
152153
}
153154
154155
.module-text {

src/components/SaveSession.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { defineComponent, onMounted, ref } from 'vue';
3535
import { saveAs } from 'file-saver';
3636
import { onKeyDown } from '@vueuse/core';
3737
38-
import { serialize } from '../io/state-file';
38+
import { serialize } from '../io/state-file/serialize';
3939
4040
const DEFAULT_FILENAME = 'session.volview.zip';
4141

src/components/tools/ScalarProbe.vue

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
<script setup lang="ts">
22
import { inject, watch, computed, toRefs } from 'vue';
33
import type { ReadonlyVec3 } from 'gl-matrix';
4+
import { vec3 } from 'gl-matrix';
45
import { onVTKEvent } from '@/src/composables/onVTKEvent';
6+
import { worldPointToIndex } from '@/src/utils/imageSpace';
57
import { VtkViewContext } from '@/src/components/vtk/context';
68
import { useCurrentImage } from '@/src/composables/useCurrentImage';
79
import vtkPointPicker from '@kitware/vtk.js/Rendering/Core/PointPicker';
@@ -110,29 +112,54 @@ const getImageSamples = (x: number, y: number) => {
110112
pointPicker.pick([x, y, 1.0], view.renderer);
111113
if (pointPicker.getActors().length === 0) return undefined;
112114
113-
const ijk = pointPicker.getPointIJK() as unknown as ReadonlyVec3;
114-
const samples = sampleSet.value.map((item: any) => {
115-
const dims = item.image.getDimensions();
116-
const scalarData = item.image.getPointData().getScalars();
117-
const index = dims[0] * dims[1] * ijk[2] + dims[0] * ijk[1] + ijk[0];
118-
const scalars = scalarData.getTuple(index) as number[];
119-
const baseInfo = { id: item.id, name: item.name };
120-
121-
if (item.type === 'segmentGroup') {
122-
return {
123-
...baseInfo,
124-
displayValues: scalars.map(
125-
(v) => item.segments.byValue[v]?.name || 'Background'
126-
),
127-
};
128-
}
129-
return { ...baseInfo, displayValues: scalars };
130-
});
131-
132-
const position = firstToSample.image.indexToWorld(ijk);
115+
// Get world position from the picked point
116+
const pickedIjk = pointPicker.getPointIJK() as unknown as ReadonlyVec3;
117+
const worldPosition = vec3.clone(
118+
firstToSample.image.indexToWorld(pickedIjk) as vec3
119+
);
120+
121+
const samples = sampleSet.value
122+
.map((item: any) => {
123+
// Convert world position to this specific image's IJK
124+
const itemIjk = worldPointToIndex(item.image, worldPosition);
125+
const dims = item.image.getDimensions();
126+
const scalarData = item.image.getPointData().getScalars();
127+
128+
// Round to nearest integer indices
129+
const i = Math.round(itemIjk[0]);
130+
const j = Math.round(itemIjk[1]);
131+
const k = Math.round(itemIjk[2]);
132+
133+
// Check bounds
134+
if (
135+
i < 0 ||
136+
j < 0 ||
137+
k < 0 ||
138+
i >= dims[0] ||
139+
j >= dims[1] ||
140+
k >= dims[2]
141+
) {
142+
return null;
143+
}
144+
145+
const index = dims[0] * dims[1] * k + dims[0] * j + i;
146+
const scalars = scalarData.getTuple(index) as number[];
147+
const baseInfo = { id: item.id, name: item.name };
148+
149+
if (item.type === 'segmentGroup') {
150+
return {
151+
...baseInfo,
152+
displayValues: scalars.map(
153+
(v) => item.segments.byValue[v]?.name || 'Background'
154+
),
155+
};
156+
}
157+
return { ...baseInfo, displayValues: scalars };
158+
})
159+
.filter((s): s is NonNullable<typeof s> => s !== null);
133160
134161
return {
135-
pos: position,
162+
pos: worldPosition,
136163
samples,
137164
};
138165
};

src/components/tools/SelectTool.vue

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import { onVTKEvent } from '@/src/composables/onVTKEvent';
33
import { WIDGET_PRIORITY } from '@kitware/vtk.js/Widgets/Core/AbstractWidget/Constants';
44
import { useToolSelectionStore } from '@/src/store/tools/toolSelection';
5+
import { useToolStore } from '@/src/store/tools';
6+
import { Tools } from '@/src/store/tools/types';
57
import { vtkAnnotationToolWidget } from '@/src/vtk/ToolWidgetUtils/types';
68
import { inject } from 'vue';
79
import { VtkViewContext } from '@/src/components/vtk/context';
@@ -10,11 +12,19 @@ const view = inject(VtkViewContext);
1012
if (!view) throw new Error('No VtkView');
1113
1214
const selectionStore = useToolSelectionStore();
15+
const toolStore = useToolStore();
16+
17+
const PLACING_TOOLS = [Tools.Ruler, Tools.Rectangle, Tools.Polygon];
1318
1419
onVTKEvent(
1520
view.interactor,
1621
'onLeftButtonPress',
1722
(event: any) => {
23+
if (PLACING_TOOLS.includes(toolStore.currentTool)) {
24+
// avoid bugs when starting a placing tool on an existing tool and right clicking and deleting existing tools
25+
return;
26+
}
27+
1828
const withModifiers = !!(event.shiftKey || event.controlKey);
1929
const selectedData = view.widgetManager.getSelectedData();
2030
if ('widget' in selectedData) {

0 commit comments

Comments
 (0)