Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion src/components/vtk/VtkSliceView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ const { metadata: imageMetadata } = useImage(imageID);
// the renderer before the renderer is deleted.
const scope = effectScope(true);
const view = scope.run(() =>
useVtkView(vtkContainerRef, { separateImageLayer: true })
useVtkView(vtkContainerRef, {
separateImageLayer: true,
clippingBounds: computed(() => imageMetadata.value.worldBounds),
})
)!;

onUnmounted(() => {
Expand Down
16 changes: 10 additions & 6 deletions src/core/vtk/useVtkView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import vtkRenderWindowInteractor from '@kitware/vtk.js/Rendering/Core/RenderWind
import vtkRenderer from '@kitware/vtk.js/Rendering/Core/Renderer';
import vtkOpenGLRenderWindow from '@kitware/vtk.js/Rendering/OpenGL/RenderWindow';
import vtkWidgetManager from '@kitware/vtk.js/Widgets/Core/WidgetManager';
import type { Bounds } from '@kitware/vtk.js/types';
import { useElementSize } from '@vueuse/core';
import {
inject,
MaybeRef,
onScopeDispose,
unref,
watchEffect,
watch,
watchPostEffect,
} from 'vue';

Expand Down Expand Up @@ -86,9 +87,14 @@ export function useWidgetManager(renderer: vtkRenderer) {
return manager;
}

type VtkViewOptions = {
separateImageLayer?: boolean;
clippingBounds?: MaybeRef<Bounds>;
};

export function useVtkView(
container: MaybeRef<Maybe<HTMLElement>>,
{ separateImageLayer = false } = {}
{ separateImageLayer = false, clippingBounds }: VtkViewOptions = {}
): View {
const parent = inject(VtkRenderWindowParentContext, null);
const renderer = vtkRenderer.newInstance();
Expand Down Expand Up @@ -185,17 +191,15 @@ export function useVtkView(
const scaledWidth = Math.max(1, width * globalThis.devicePixelRatio);
const scaledHeight = Math.max(1, height * globalThis.devicePixelRatio);
renderWindowView.setSize(scaledWidth, scaledHeight);
imageRenderer.resetCameraClippingRange();
imageRenderer.resetCameraClippingRange(unref(clippingBounds));

parent?.renderWindowView.resizeFromChildRenderWindows();
parent?.renderWindow.render();
requestRender({ immediate: true });
};

const { width, height } = useElementSize(container);
watchEffect(() => {
setSize(width.value, height.value);
});
watch([width, height], ([w, h]) => setSize(w, h), { immediate: true });

// cleanup
onScopeDispose(() => {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/baseline/paint_tool_axial_view_after_stroke-chrome-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 14 additions & 4 deletions tests/specs/different-direction-labelmap.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PROSTATE_610_LABELMAP_MANIFEST } from './configTestUtils';
import { writeManifestToFile } from './utils';
import { moveTo } from './annotationTestUtils';
import { volViewPage } from '../pageobjects/volview.page';
import {
openAnnotationSegments,
Expand All @@ -8,6 +9,8 @@ import {
waitForSegmentContent,
} from './segmentationTestUtils';

const MAX_MISMATCH_PERCENT = 0.01;

/**
* Regression test for labelmap with different direction matrix than parent image.
*
Expand Down Expand Up @@ -73,18 +76,25 @@ describe('Labelmap with different direction matrix', () => {
.up()
.perform();

await moveTo(10, 10);
const annotations = await coronalView.$('.view-annotations');
await browser.waitUntil(
async () => {
const result = await browser.checkElement(
coronalView,
'different_direction_labelmap_paint_coronal'
'different_direction_labelmap_paint_coronal',
{
biDiOrigin: 'viewport',
removeElements: [annotations],
rawMisMatchPercentage: true,
saveAboveTolerance: MAX_MISMATCH_PERCENT,
}
);
return (result as number) < 5;
return (result as number) <= MAX_MISMATCH_PERCENT;
},
{
timeout: 10000,
timeoutMsg:
'Paint stroke on coronal view with misaligned labelmap should match baseline',
timeoutMsg: `Painted coronal view with misaligned labelmap should match its baseline within ${MAX_MISMATCH_PERCENT}%`,
interval: 1000,
}
);
Expand Down
70 changes: 54 additions & 16 deletions tests/specs/layer-display-controls.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ async function openDisplayFixture({
mappingRange = [-2.75, 100.25],
opacity = 0.5,
transparentBackground = false,
singleSlice = false,
} = {}) {
const display = { preset, mappingRange, opacity, transparentBackground };
const base = writeMetaImage('display-base.mha');
Expand All @@ -128,6 +129,25 @@ async function openDisplayFixture({
await writeManifestToFile(
{
...layeredImageManifest(base, overlay),
...(singleSlice
? {
layout: {
direction: 'row',
items: [{ type: 'slot', slotIndex: 0 }],
},
layoutSlots: ['axial'],
activeView: 'axial',
viewByID: {
axial: {
id: 'axial',
type: '2D',
name: 'Axial',
dataID: 'base',
options: { orientation: 'Axial' },
},
},
}
: {}),
layerDisplay: {
'base::overlay': display,
},
Expand All @@ -149,18 +169,20 @@ async function openDisplayFixture({
async () => {
const pixels = await captureViewPixels();
return (
pixels.length === 3 && pixels.every(({ colored }) => colored > 1000)
pixels.length === (singleSlice ? 1 : 3) &&
pixels.every(({ colored }) => colored > 1000)
);
},
{ timeoutMsg: 'Expected the overlay to render in all three 2D views' }
{ timeoutMsg: 'Expected the overlay to render in every 2D view' }
);
}

async function openEditedDisplayFixture() {
async function openEditedDisplayFixture(singleSlice = false) {
await openDisplayFixture({
preset: 'DICOM Hot Iron',
mappingRange: [0, 255],
opacity: 0.6,
singleSlice,
});
const select = $('[data-testid="layer-colormap"]');
await select.scrollIntoView({ block: 'center', behavior: 'instant' });
Expand All @@ -185,12 +207,12 @@ async function openEditedDisplayFixture() {
async () => {
const pixels = await captureViewPixels();
return (
pixels.length === 3 &&
pixels.length === (singleSlice ? 1 : 3) &&
pixels.every(({ colored, blue }) => colored > 1000 && blue > 0)
);
},
{
timeoutMsg: 'Expected the edited Cool to Warm palette in all three views',
timeoutMsg: 'Expected the edited Cool to Warm palette in every 2D view',
}
);
const baseWindowing = await windowingLabels();
Expand All @@ -199,6 +221,20 @@ async function openEditedDisplayFixture() {
return { baseWindowing, appliedPixels };
}

async function convertSlicesToVolumes() {
const views = await volViewPage.getViews2D();
const count = await views.length;
expect(count).toBeGreaterThan(0);
for (let remaining = count - 1; remaining >= 0; remaining--) {
const view = (await volViewPage.getViews2D())[0];
await view.$('.view-type-select').click();
const volume = $('[role="option"]*=Volume');
await volume.waitForClickable();
await volume.click();
await volViewPage.waitForViewCounts(remaining, true);
}
}

describe('Overlay display controls', () => {
for (const { title, preset } of [
{ title: 'missing', preset: undefined },
Expand Down Expand Up @@ -536,24 +572,26 @@ describe('Overlay display controls', () => {
await expectRenderedPixels(appliedPixels);
});

it('restores global appearance after converting every 2D view and saving a 3D-only session', async () => {
it('restores global appearance after converting every 2D view', async () => {
const { baseWindowing, appliedPixels } = await openEditedDisplayFixture();
for (let remaining = 2; remaining >= 0; remaining--) {
const view = (await volViewPage.getViews2D())[0];
await view.$('.view-type-select').click();
const volume = $('[role="option"]*=Volume');
await volume.waitForClickable();
await volume.click();
await volViewPage.waitForViewCounts(remaining, true);
}
await selectLayout('3D Only', 0, true);
await convertSlicesToVolumes();
await selectLayout('Four Up', 3, true);
await expectDisplaySettings('-2.75', '100.25', '100.3');
expect(await windowingLabels()).toEqual(baseWindowing);
await expectRenderedPixels(appliedPixels);
});

it('restores global appearance from a session saved after removing the last 2D view', async () => {
const { baseWindowing, appliedPixels } =
await openEditedDisplayFixture(true);
await convertSlicesToVolumes();
const saved = await volViewPage.saveSession();
await waitForDownload(path.join(TEMP_DIR, saved), SESSION_SAVE_TIMEOUT);
await openVolViewPage(saved);
await volViewPage.waitForViewCounts(0, true);
await openLayerDisplayControls();
await expect($('[data-testid="layer-range-min"] input')).toBeEnabled();
await selectLayout('Four Up', 3, true);
await selectLayout('Axial Only', 1, false);
await expectDisplaySettings('-2.75', '100.25', '100.3');
expect(await windowingLabels()).toEqual(baseWindowing);
await expectRenderedPixels(appliedPixels);
Expand Down
40 changes: 24 additions & 16 deletions tests/specs/paint-tool-rendering.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { PROSTATEX_DATASET } from '../datasets';
import { openUrls } from './utils';
import { moveTo } from './annotationTestUtils';

const MAX_MISMATCH_PERCENT = 0.01;

describe('Paint tool rendering', () => {
it('should not black out axial view after painting', async () => {
await openUrls([PROSTATEX_DATASET]);
Expand Down Expand Up @@ -34,22 +36,6 @@ describe('Paint tool rendering', () => {
.perform();
}

await browser.waitUntil(
async () => {
const result = await browser.checkElement(
axialView,
'paint_tool_axial_view_after_stroke'
);
return (result as number) < 5;
},
{
timeout: 10000,
timeoutMsg:
'Axial view should not go black after painting with paint tool',
interval: 1000,
}
);

const canvasImage = () =>
browser.execute(
(element) => (element as HTMLCanvasElement).toDataURL(),
Expand All @@ -61,6 +47,28 @@ describe('Paint tool rendering', () => {
timeoutMsg: 'Brush preview should disappear when leaving the view',
});
const withoutPreview = await canvasImage();
const annotations = await axialView.$('.view-annotations');

await browser.waitUntil(
async () => {
const result = await browser.checkElement(
axialView,
'paint_tool_axial_view_after_stroke',
{
biDiOrigin: 'viewport',
removeElements: [annotations],
rawMisMatchPercentage: true,
saveAboveTolerance: MAX_MISMATCH_PERCENT,
}
);
return (result as number) <= MAX_MISMATCH_PERCENT;
},
{
timeout: 10000,
timeoutMsg: `Painted axial view should match its baseline within ${MAX_MISMATCH_PERCENT}%`,
interval: 1000,
}
);

await moveTo(centerX - 60, centerY);
await browser.waitUntil(
Expand Down
38 changes: 25 additions & 13 deletions tests/specs/slice-display-lifecycle.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ const captureSlicePixels = () =>
captureViewPixels('[data-testid~="vtk-two-view"]', 40, 'slice-lifecycle.png');
const obliqueSlices =
'.oblique-grid > .d-flex:not(:nth-child(2)) [data-testid="vtk-view"]';
const captureOblique = () =>
captureViewPixels(obliqueSlices, 40, 'slice-lifecycle-oblique.png');
const captureOblique = (fileName = 'slice-lifecycle-oblique.png') =>
captureViewPixels(obliqueSlices, 40, fileName);

const coloredPixelCount = (data: Buffer) =>
data.filter(
Expand Down Expand Up @@ -305,10 +305,14 @@ describe('Slice display lifecycle', () => {
pixels.every(({ data }) => coloredPixelCount(data) > 1000)
);
});
const baseImages = await captureOblique();
const baseImages = await captureOblique(
`slice-lifecycle-oblique-before-${cycle}.png`
);
await toggleVisibility('base-image');
await browser.waitUntil(async () => {
const pixels = await captureOblique();
const pixels = await captureOblique(
`slice-lifecycle-oblique-hidden-${cycle}.png`
);
return (
pixels.length === 3 &&
pixels.every(
Expand All @@ -319,15 +323,23 @@ describe('Slice display lifecycle', () => {
);
});
await toggleVisibility('base-image');
await browser.waitUntil(async () => {
const pixels = await captureOblique();
return (
pixels.length === 3 &&
pixels.every(({ data }, index) =>
data.equals(baseImages[index].data)
)
);
});
await browser.waitUntil(
async () => {
const pixels = await captureOblique(
`slice-lifecycle-oblique-restored-${cycle}.png`
);
return (
pixels.length === 3 &&
pixels.every(({ data }, index) =>
data.equals(baseImages[index].data)
)
);
},
{
timeoutMsg:
'Expected restored oblique slices to match their visible appearance',
}
);
await selectLayout('Four Up', 3, true);
await expectSlicesMatch(fourUp);
}
Expand Down
Loading