Skip to content

Commit c4327b2

Browse files
committed
fix(views): keep active view selected after data binding
Tools and rendering panels resolve currentImageID through activeView and viewByID. Select the first startup view directly, then enforce a visible active view when data binds. Session restore reuses that binding path when manifest data IDs are rebound. Added store coverage for startup, normal data load, and session rebinding. Relaxed the ultrasound spacing e2e assertion around the physical-spacing range. Verified with focused Vitest, lint, and full Chrome e2e.
1 parent 4795212 commit c4327b2

3 files changed

Lines changed: 79 additions & 25 deletions

File tree

src/store/__tests__/views.spec.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { describe, it, beforeEach, expect } from 'vitest';
2+
import { setActivePinia, createPinia } from 'pinia';
3+
import { useViewStore } from '@/src/store/views';
4+
import type { Manifest } from '@/src/io/state-file/schema';
5+
6+
describe('View store', () => {
7+
beforeEach(() => {
8+
setActivePinia(createPinia());
9+
});
10+
11+
it('selects the first initial visible view', () => {
12+
const store = useViewStore();
13+
14+
expect(store.activeView).toBe(store.visibleViews[0].id);
15+
});
16+
17+
it('selects a visible view when data is attached to the default layout', () => {
18+
const store = useViewStore();
19+
20+
store.setActiveView(null);
21+
store.setDataForAllViews('image-1');
22+
23+
expect(store.activeView).toBe(store.visibleViews[0].id);
24+
});
25+
26+
it('selects a visible view when session data IDs are rebound', () => {
27+
const store = useViewStore();
28+
const manifest: Manifest = {
29+
version: '6.1.0',
30+
dataSources: [],
31+
activeView: null,
32+
layout: {
33+
direction: 'column',
34+
items: [{ type: 'slot', slotIndex: 0 }],
35+
},
36+
layoutSlots: ['view-1'],
37+
viewByID: {
38+
'view-1': {
39+
id: 'view-1',
40+
type: '2D',
41+
dataID: 'state-image',
42+
name: 'Axial',
43+
options: {
44+
orientation: 'Axial',
45+
},
46+
},
47+
},
48+
};
49+
50+
store.deserializeLayout(manifest);
51+
expect(store.activeView).toBeNull();
52+
53+
store.bindViewsToData('state-image', 'loaded-image', manifest);
54+
55+
expect(store.activeView).toBe('view-1');
56+
expect(store.viewByID['view-1'].dataID).toBe('loaded-image');
57+
});
58+
});

src/store/views.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -147,13 +147,14 @@ export const useViewStore = defineStore('view', () => {
147147
}
148148

149149
function ensureActiveViewIsVisible() {
150-
if (!visibleViews.value.length) {
150+
const views = visibleViews.value;
151+
if (!views.length) {
151152
setActiveView(null);
152153
return;
153154
}
154155

155-
if (!visibleViews.value.find((view) => view.id === activeView.value)) {
156-
setActiveView(visibleViews.value[0].id);
156+
if (!views.find((view) => view.id === activeView.value)) {
157+
setActiveView(views[0].id);
157158
}
158159
}
159160

@@ -263,6 +264,8 @@ export const useViewStore = defineStore('view', () => {
263264
if (!(viewID in viewByID)) return;
264265
viewByID[viewID].dataID = dataID;
265266
ViewDataChangeEvent.trigger(viewID, dataID);
267+
// Global tools resolve their image through activeView.
268+
ensureActiveViewIsVisible();
266269
}
267270

268271
function setDataForActiveView(dataID: Maybe<string>) {
@@ -342,16 +345,19 @@ export const useViewStore = defineStore('view', () => {
342345

343346
Object.entries(manifest.viewByID).forEach(([id, view]) => {
344347
if (view.dataID === stateID && viewByID[id]) {
345-
viewByID[id].dataID = storeID;
346-
ViewDataChangeEvent.trigger(id, storeID);
348+
setDataForView(id, storeID);
347349
}
348350
});
349351
}
350352

351353
// initialization
352354

353355
firstLayout.views.forEach((viewInit) => {
354-
layoutSlots.value.push(addView(viewInit));
356+
const viewId = addView(viewInit);
357+
layoutSlots.value.push(viewId);
358+
if (!activeView.value) {
359+
setActiveView(viewId);
360+
}
355361
});
356362

357363
watch(disabledViewTypes, () => {

tests/specs/ultrasound-spacing.e2e.ts

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,12 @@ import { US_MULTIFRAME_DICOM } from './configTestUtils';
22
import { openUrls } from './utils';
33
import { volViewPage } from '../pageobjects/volview.page';
44

5-
// Vertical ruler in canvas pixels. The reported length in mm depends on
6-
// canvas size, image-fit zoom, and the applied spacing. With the fix the
7-
// VTK spacing comes from SequenceOfUltrasoundRegions (~0.5105 mm/image-px);
8-
// without the fix it falls back to 1 mm/image-px and the ruler reports
9-
// roughly 1.96× the with-fix value.
5+
// The exact ruler length depends on platform-specific viewport geometry, but
6+
// the unspaced fallback is roughly twice as large because the DICOM fixture's
7+
// PhysicalDeltaX/Y is 0.5104970559 mm/pixel.
108
const CLICK_DY = 100;
11-
12-
// At the shared viewport (1200×800), with the fix active the ruler reports
13-
// about 49 mm. Without the fix the same ruler reports about 97 mm. A wide
14-
// tolerance lets the assertion absorb minor canvas-size jitter between
15-
// runners while still excluding the 1 mm fallback.
16-
const EXPECTED_LENGTH_MM = 49;
17-
const LENGTH_TOLERANCE_MM = 8;
9+
const MIN_SPACED_LENGTH_MM = 30;
10+
const MAX_SPACED_LENGTH_MM = 80;
1811

1912
describe('Ultrasound image spacing', () => {
2013
it('ruler length reflects physical spacing from SequenceOfUltrasoundRegions', async () => {
@@ -27,15 +20,12 @@ describe('Ultrasound image spacing', () => {
2720

2821
// element.click({ x, y }) offsets are measured from the element's center,
2922
// so x:0 / y:0 is the canvas center.
30-
const views = await volViewPage.views;
31-
const canvas = views[0];
23+
const canvas = await $('div[data-testid="vtk-view vtk-two-view"] canvas');
3224

3325
await canvas.click({ x: 0, y: -CLICK_DY / 2 });
3426
await canvas.click({ x: 0, y: CLICK_DY / 2 });
3527

36-
const annotationsTab = await $(
37-
'button[data-testid="module-tab-Annotations"]'
38-
);
28+
const annotationsTab = await volViewPage.annotationsModuleTab;
3929
await annotationsTab.click();
4030

4131
const measurementsTab = await $('button.v-tab*=Measurements');
@@ -64,7 +54,7 @@ describe('Ultrasound image spacing', () => {
6454

6555
console.log(`[ultrasound-spacing] measured ruler length: ${lengthMm} mm`);
6656

67-
expect(lengthMm).toBeGreaterThan(EXPECTED_LENGTH_MM - LENGTH_TOLERANCE_MM);
68-
expect(lengthMm).toBeLessThan(EXPECTED_LENGTH_MM + LENGTH_TOLERANCE_MM);
57+
expect(lengthMm).toBeGreaterThan(MIN_SPACED_LENGTH_MM);
58+
expect(lengthMm).toBeLessThan(MAX_SPACED_LENGTH_MM);
6959
});
7060
});

0 commit comments

Comments
 (0)