Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f5e1688
feat(tools): hover over tool fill shows tooltip
PaulHax Nov 29, 2025
53629e1
fix(tools): prevent tool selection while placing annotation tools
PaulHax Nov 29, 2025
d2c2282
feat: support sparse manifest.json in state files
PaulHax Nov 29, 2025
b8ce0cd
fix: report errors and support multiple volumes in state file restora…
PaulHax Nov 30, 2025
9bbdba9
fix: unify state file restoration with main import pipeline
PaulHax Nov 30, 2025
c990ab9
refactor(e2e): consolidate test utilities and fix slow state-manifest…
PaulHax Nov 30, 2025
d2f9566
refactor: extract state file migrations to dedicated module
PaulHax Nov 30, 2025
7bf7da4
refactor: simplify state file data source restoration
PaulHax Nov 30, 2025
7a69dfc
refactor: replace state file callbacks with result-based flow
PaulHax Nov 30, 2025
b54cea8
fix: prevent VTK crash when rendering images without scalar data
PaulHax Nov 30, 2025
ac09cd9
chore(e2e): speed up tests
PaulHax Nov 30, 2025
7043c04
test(e2e): add sparse manifest test for prostate rectangle with lesio…
PaulHax Nov 30, 2025
e587603
fix(state): bump manifest version to 6.1.1 for sparse schema support
PaulHax Dec 1, 2025
e23a244
feat(state): support standalone JSON state files without zip wrapper
PaulHax Dec 1, 2025
06e9d5f
fix(paint): use labelmap's coordinate transform
PaulHax Dec 1, 2025
735dea8
fix(segmentation): handle labelmaps with different orientations
PaulHax Dec 2, 2025
bd05fb8
feat(state): support remote URIs for segment groups
PaulHax Dec 2, 2025
fa4deed
test: add regression test for labelmap with different direction matrix
PaulHax Dec 2, 2025
e29215c
fix(state): use primarySelection when restoring view data
PaulHax Dec 2, 2025
af7bff1
fix(urlParams): don't split URLs containing commas in query strings
PaulHax Dec 2, 2025
9b86b85
feat(labels): put each label on own row
PaulHax Dec 3, 2025
820ad2d
fix(tools): keep segment groups from covering rect and poly fills
PaulHax Dec 3, 2025
9b8c30b
fix(tools): show hover labels while placing annotation tools
PaulHax Dec 3, 2025
4ad0470
refactor: rename labelMaps to segmentGroups in state file schema
PaulHax Dec 3, 2025
66d4f4c
refactor: rename state-file/index.ts to serialize.ts
PaulHax Dec 3, 2025
f5ee37e
refactor: replace session-zip test with session-state-lifecycle
PaulHax Dec 3, 2025
925e41e
refactor(state): drop support for state files v3.0.0 and earlier
PaulHax Dec 3, 2025
0cc85d6
docs: document sparse manifest state files
PaulHax Dec 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
name: E2E Testing on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
env:
DOWNLOAD_TIMEOUT: 220000
DOWNLOAD_TIMEOUT: 60000
VITE_SHOW_SAMPLE_DATA: true
steps:
- uses: actions/checkout@v4
Expand Down
4 changes: 4 additions & 0 deletions docs/loading_data.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,7 @@ To layer images:
1. Under the Rendering tab, an opacity slider changes the transparency of the upper layer.

![Add Layer](./assets/add-layer.jpg)

## State Files

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`).
68 changes: 63 additions & 5 deletions docs/state_files.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,69 @@
# State Files

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.
VolView state files save your scene configuration: annotations, camera positions, colormaps, layouts, and more. There are two formats:

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.
## Zip State Files (`*.volview.zip`)

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.
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.

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.
## Sparse Manifest Files (`*.volview.json`)

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.
JSON files that reference remote data via URIs instead of embedding it. Useful for:

- Linking to data hosted on servers
- Sharing annotations without duplicating large datasets
- Integrating with external systems (AI pipelines, access control, etc.)

Example manifest:

```json
{
"version": "6.2.0",
"dataSources": [
{ "id": 0, "type": "uri", "uri": "https://example.com/scan.zip" },
{ "id": 1, "type": "uri", "uri": "https://example.com/segmentation.nii.gz" }
],
"segmentGroups": [
{
"id": "seg-1",
"dataSourceId": 1,
"metadata": {
"name": "Tumor Segmentation",
"parentImage": "0",
"segments": {
"order": [1],
"byValue": {
"1": { "value": 1, "name": "Tumor", "color": [255, 0, 0, 255] }
}
}
}
}
],
"tools": {
"rectangles": {
"tools": [
{
"imageID": "0",
"frameOfReference": {
"planeNormal": [0, 0, 1],
"planeOrigin": [0, 0, 50]
},
"slice": 50,
"firstPoint": [-20, -20, 50],
"secondPoint": [20, 20, 50],
"label": "lesion"
}
],
"labels": {
"lesion": { "color": "red" }
}
}
}
}
```

## Loading State Files

- **Drag and drop** onto VolView
- **File browser** via the "Folder" icon below the save button
- **URL parameter**: `?urls=[https://example.com/session.volview.json]`
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions src/components/EditableChipList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ const itemsToRender = computed(() =>
mandatory
>
<v-row dense>
<v-col cols="6" v-for="({ key, title }, idx) in itemsToRender" :key="key">
<v-col
cols="12"
v-for="({ key, title }, idx) in itemsToRender"
:key="key"
>
<v-item v-slot="{ selectedClass, toggle }" :value="key">
<v-chip
variant="tonal"
Expand All @@ -59,7 +63,7 @@ const itemsToRender = computed(() =>
</v-col>

<!-- Add Label button -->
<v-col cols="6">
<v-col cols="12">
<v-chip variant="outlined" class="w-100" @click="$emit('create')">
<v-icon class="mr-2">mdi-plus</v-icon>
{{ createLabelText }}
Expand Down
1 change: 1 addition & 0 deletions src/components/ModulePanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export default defineComponent({
position: relative;
flex: 2;
overflow: auto;
scrollbar-gutter: stable;
}

.module-text {
Expand Down
2 changes: 1 addition & 1 deletion src/components/SaveSession.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { defineComponent, onMounted, ref } from 'vue';
import { saveAs } from 'file-saver';
import { onKeyDown } from '@vueuse/core';

import { serialize } from '../io/state-file';
import { serialize } from '../io/state-file/serialize';

const DEFAULT_FILENAME = 'session.volview.zip';

Expand Down
69 changes: 48 additions & 21 deletions src/components/tools/ScalarProbe.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<script setup lang="ts">
import { inject, watch, computed, toRefs } from 'vue';
import type { ReadonlyVec3 } from 'gl-matrix';
import { vec3 } from 'gl-matrix';
import { onVTKEvent } from '@/src/composables/onVTKEvent';
import { worldPointToIndex } from '@/src/utils/imageSpace';
import { VtkViewContext } from '@/src/components/vtk/context';
import { useCurrentImage } from '@/src/composables/useCurrentImage';
import vtkPointPicker from '@kitware/vtk.js/Rendering/Core/PointPicker';
Expand Down Expand Up @@ -110,29 +112,54 @@ const getImageSamples = (x: number, y: number) => {
pointPicker.pick([x, y, 1.0], view.renderer);
if (pointPicker.getActors().length === 0) return undefined;

const ijk = pointPicker.getPointIJK() as unknown as ReadonlyVec3;
const samples = sampleSet.value.map((item: any) => {
const dims = item.image.getDimensions();
const scalarData = item.image.getPointData().getScalars();
const index = dims[0] * dims[1] * ijk[2] + dims[0] * ijk[1] + ijk[0];
const scalars = scalarData.getTuple(index) as number[];
const baseInfo = { id: item.id, name: item.name };

if (item.type === 'segmentGroup') {
return {
...baseInfo,
displayValues: scalars.map(
(v) => item.segments.byValue[v]?.name || 'Background'
),
};
}
return { ...baseInfo, displayValues: scalars };
});

const position = firstToSample.image.indexToWorld(ijk);
// Get world position from the picked point
const pickedIjk = pointPicker.getPointIJK() as unknown as ReadonlyVec3;
const worldPosition = vec3.clone(
firstToSample.image.indexToWorld(pickedIjk) as vec3
);

const samples = sampleSet.value
.map((item: any) => {
// Convert world position to this specific image's IJK
const itemIjk = worldPointToIndex(item.image, worldPosition);
const dims = item.image.getDimensions();
const scalarData = item.image.getPointData().getScalars();

// Round to nearest integer indices
const i = Math.round(itemIjk[0]);
const j = Math.round(itemIjk[1]);
const k = Math.round(itemIjk[2]);

// Check bounds
if (
i < 0 ||
j < 0 ||
k < 0 ||
i >= dims[0] ||
j >= dims[1] ||
k >= dims[2]
) {
return null;
}

const index = dims[0] * dims[1] * k + dims[0] * j + i;
const scalars = scalarData.getTuple(index) as number[];
const baseInfo = { id: item.id, name: item.name };

if (item.type === 'segmentGroup') {
return {
...baseInfo,
displayValues: scalars.map(
(v) => item.segments.byValue[v]?.name || 'Background'
),
};
}
return { ...baseInfo, displayValues: scalars };
})
.filter((s): s is NonNullable<typeof s> => s !== null);

return {
pos: position,
pos: worldPosition,
samples,
};
};
Expand Down
10 changes: 10 additions & 0 deletions src/components/tools/SelectTool.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import { onVTKEvent } from '@/src/composables/onVTKEvent';
import { WIDGET_PRIORITY } from '@kitware/vtk.js/Widgets/Core/AbstractWidget/Constants';
import { useToolSelectionStore } from '@/src/store/tools/toolSelection';
import { useToolStore } from '@/src/store/tools';
import { Tools } from '@/src/store/tools/types';
import { vtkAnnotationToolWidget } from '@/src/vtk/ToolWidgetUtils/types';
import { inject } from 'vue';
import { VtkViewContext } from '@/src/components/vtk/context';
Expand All @@ -10,11 +12,19 @@ const view = inject(VtkViewContext);
if (!view) throw new Error('No VtkView');

const selectionStore = useToolSelectionStore();
const toolStore = useToolStore();

const PLACING_TOOLS = [Tools.Ruler, Tools.Rectangle, Tools.Polygon];

onVTKEvent(
view.interactor,
'onLeftButtonPress',
(event: any) => {
if (PLACING_TOOLS.includes(toolStore.currentTool)) {
// avoid bugs when starting a placing tool on an existing tool and right clicking and deleting existing tools
return;
}

const withModifiers = !!(event.shiftKey || event.controlKey);
const selectedData = view.widgetManager.getSelectedData();
if ('widget' in selectedData) {
Expand Down
Loading
Loading