From e1ed9b12c2207606293cc70c5f84bb5631dd0ccf Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Wed, 21 Oct 2026 09:04:23 -0400 Subject: [PATCH 1/5] feat(rendering): add reusable range and palette controls Provide shared palette selection, numeric display limits, and a draggable three-handle range bar with Tab and arrow-key interaction. --- src/components/ColorMapRangeControl.vue | 330 ++++++ src/components/ColorMapSelect.vue | 64 ++ src/components/ColorMapSwatch.vue | 33 + src/components/DisplayRangeControl.vue | 202 ++++ src/components/DisplayRangeInput.vue | 109 ++ src/components/ImageDisplayControls.vue | 183 +++ src/composables/useDisplayRangeSlider.ts | 221 ++++ src/utils/adjacentNumber.ts | 9 + src/utils/colorMapGradient.ts | 76 ++ src/utils/formatDisplayRange.ts | 25 + src/utils/isValidDisplayRange.ts | 7 + src/vtk/ColorMaps.ts | 19 +- src/vtk/DicomColorPresets.json | 1312 ++++++++++++++++++++++ src/vtk/LayerColorPresets.ts | 25 + vite.config.ts | 9 +- 15 files changed, 2610 insertions(+), 14 deletions(-) create mode 100644 src/components/ColorMapRangeControl.vue create mode 100644 src/components/ColorMapSelect.vue create mode 100644 src/components/ColorMapSwatch.vue create mode 100644 src/components/DisplayRangeControl.vue create mode 100644 src/components/DisplayRangeInput.vue create mode 100644 src/components/ImageDisplayControls.vue create mode 100644 src/composables/useDisplayRangeSlider.ts create mode 100644 src/utils/adjacentNumber.ts create mode 100644 src/utils/colorMapGradient.ts create mode 100644 src/utils/formatDisplayRange.ts create mode 100644 src/utils/isValidDisplayRange.ts create mode 100644 src/vtk/DicomColorPresets.json create mode 100644 src/vtk/LayerColorPresets.ts diff --git a/src/components/ColorMapRangeControl.vue b/src/components/ColorMapRangeControl.vue new file mode 100644 index 000000000..e41691e83 --- /dev/null +++ b/src/components/ColorMapRangeControl.vue @@ -0,0 +1,330 @@ + + + + + diff --git a/src/components/ColorMapSelect.vue b/src/components/ColorMapSelect.vue new file mode 100644 index 000000000..dd83dc8a9 --- /dev/null +++ b/src/components/ColorMapSelect.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/src/components/ColorMapSwatch.vue b/src/components/ColorMapSwatch.vue new file mode 100644 index 000000000..a04201471 --- /dev/null +++ b/src/components/ColorMapSwatch.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/src/components/DisplayRangeControl.vue b/src/components/DisplayRangeControl.vue new file mode 100644 index 000000000..f2b761c00 --- /dev/null +++ b/src/components/DisplayRangeControl.vue @@ -0,0 +1,202 @@ + + + + + diff --git a/src/components/DisplayRangeInput.vue b/src/components/DisplayRangeInput.vue new file mode 100644 index 000000000..a164d4607 --- /dev/null +++ b/src/components/DisplayRangeInput.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/src/components/ImageDisplayControls.vue b/src/components/ImageDisplayControls.vue new file mode 100644 index 000000000..4c6ee6417 --- /dev/null +++ b/src/components/ImageDisplayControls.vue @@ -0,0 +1,183 @@ + + + + + diff --git a/src/composables/useDisplayRangeSlider.ts b/src/composables/useDisplayRangeSlider.ts new file mode 100644 index 000000000..517b9a10d --- /dev/null +++ b/src/composables/useDisplayRangeSlider.ts @@ -0,0 +1,221 @@ +import { computed, onBeforeUnmount, ref } from 'vue'; +import { adjacentNumber } from '@/src/utils/adjacentNumber'; +import { isValidDisplayRange } from '@/src/utils/isValidDisplayRange'; + +export type DisplayRange = readonly [number, number]; +type Handle = 'min' | 'max' | 'center'; + +type Drag = { + handle: Handle; + pointerId: number; + startX: number; + width: number; + dataRange: DisplayRange; + moved: boolean; + range: DisplayRange; + target: HTMLButtonElement; +}; + +type Options = { + modelValue: DisplayRange; + dataRange: DisplayRange; + disabled?: boolean; + windowLevel?: boolean; +}; + +export function useDisplayRangeSlider( + props: Options, + updateRange: (range: [number, number]) => void +) { + const track = ref(); + let drag: Drag | undefined; + const clamp = (value: number, low: number, high: number) => + Math.max(low, Math.min(high, value)); + const span = computed(() => props.dataRange[1] - props.dataRange[0]); + + const valid = computed( + () => + isValidDisplayRange(props.dataRange) && + isValidDisplayRange(props.modelValue) + ); + + const endpointLimit = (value: number, direction: number) => { + const next = adjacentNumber(value, direction); + // A window/level pair needs a representable midpoint between its endpoints. + return props.windowLevel ? adjacentNumber(next, direction) : next; + }; + + const bounds = ( + handle: Handle, + range: DisplayRange = props.modelValue, + domain: DisplayRange = props.dataRange + ): DisplayRange => { + const [low, high] = domain; + if (!valid.value) return [0, 0]; + if (handle === 'min') + return [low, Math.min(high, endpointLimit(range[1], -1))]; + if (handle === 'max') + return [Math.max(low, endpointLimit(range[0], 1)), high]; + const halfWidth = (range[1] - range[0]) / 2; + const limit = Number.MAX_VALUE - halfWidth; + const first = clamp(low + halfWidth, -limit, limit); + const second = clamp(high - halfWidth, -limit, limit); + // A wide window can move while continuing to cover the image range. + return [Math.min(first, second), Math.max(first, second)]; + }; + + const value = (handle: Handle, range: DisplayRange = props.modelValue) => { + if (!isValidDisplayRange(range)) return 0; + if (handle === 'min') return range[0]; + if (handle === 'max') return range[1]; + return range[0] + (range[1] - range[0]) / 2; + }; + + const interactionBounds = ( + handle: Handle, + range: DisplayRange = props.modelValue, + domain: DisplayRange = props.dataRange + ): DisplayRange => { + const [low, high] = bounds(handle, range, domain); + const current = value(handle, range); + // Preserve an out-of-range starting value until the gesture brings it inward. + return [Math.min(low, current), Math.max(high, current)]; + }; + + const disabledReason = () => { + if (props.disabled) return 'Display range controls are unavailable.'; + if (!valid.value) + return 'A finite, nonconstant image and display range is required.'; + return ''; + }; + + const overflowDirection = (handle: Handle) => { + if (!valid.value) return undefined; + const current = value(handle); + if (current < props.dataRange[0]) return 'left'; + if (current > props.dataRange[1]) return 'right'; + return undefined; + }; + + const valueText = (handle: Handle) => { + const outside = overflowDirection(handle); + return outside + ? String(value(handle)) + + (outside === 'left' ? ' (below image range)' : ' (above image range)') + : String(value(handle)); + }; + + const positionFraction = (handle: Handle) => + valid.value + ? (clamp(value(handle), ...props.dataRange) - props.dataRange[0]) / + span.value + : 0.5; + + const update = ( + handle: Handle, + target: number, + range: DisplayRange, + domain: DisplayRange = props.dataRange + ) => { + if (disabledReason() || !Number.isFinite(target)) return; + const [low, high] = interactionBounds(handle, range, domain); + const next = clamp(target, low, high); + let result: [number, number]; + if (handle === 'min') result = [next, range[1]]; + else if (handle === 'max') result = [range[0], next]; + else { + const width = range[1] - range[0]; + const first = Math.min(domain[0], Number.MAX_VALUE - width); + const second = Math.max(-Number.MAX_VALUE, domain[1] - width); + const delta = next - value('center', range); + const minimum = clamp( + Number.isFinite(delta) ? range[0] + delta : next - width / 2, + Math.min(first, second, range[0]), + Math.max(first, second, range[0]) + ); + result = [minimum, minimum + width]; + } + if ( + isValidDisplayRange(result) && + result.some((endpoint, index) => endpoint !== props.modelValue[index]) + ) + updateRange(result); + }; + + const endDrag = () => { + const previous = drag; + drag = undefined; + if (previous?.target.hasPointerCapture(previous.pointerId)) + previous.target.releasePointerCapture(previous.pointerId); + }; + + const beginDrag = (event: PointerEvent, handle: Handle) => { + if (drag || disabledReason() || event.button !== 0) return; + const width = track.value?.getBoundingClientRect().width; + if (!width) return; + const target = event.currentTarget as HTMLButtonElement; + target.focus({ preventScroll: true }); + target.setPointerCapture(event.pointerId); + drag = { + handle, + target, + pointerId: event.pointerId, + startX: event.clientX, + width, + dataRange: [...props.dataRange], + moved: false, + range: [...props.modelValue], + }; + }; + + const moveDrag = (event: PointerEvent) => { + if (!drag || event.pointerId !== drag.pointerId) return; + if (!drag.moved && event.clientX === drag.startX) return; + drag.moved = true; + const scalarSpan = drag.dataRange[1] - drag.dataRange[0]; + const delta = ((event.clientX - drag.startX) / drag.width) * scalarSpan; + update( + drag.handle, + value(drag.handle, drag.range) + delta, + drag.range, + drag.dataRange + ); + }; + + const releaseDrag = (event: PointerEvent) => { + if (drag?.pointerId === event.pointerId) endDrag(); + }; + + const onKeydown = (event: KeyboardEvent, handle: Handle) => { + const directions: Record = { + ArrowLeft: -1, + ArrowDown: -1, + ArrowRight: 1, + ArrowUp: 1, + }; + const direction = directions[event.key]; + if (!direction) return; + event.preventDefault(); + event.stopPropagation(); + const current = value(handle); + let next = current + (span.value / 100) * direction; + if (next === current) next = adjacentNumber(current, direction); + update(handle, next, props.modelValue); + }; + + onBeforeUnmount(endDrag); + + return { + track, + positionFraction, + interactionBounds, + value, + valueText, + overflowDirection, + disabledReason, + beginDrag, + moveDrag, + releaseDrag, + onKeydown, + }; +} diff --git a/src/utils/adjacentNumber.ts b/src/utils/adjacentNumber.ts new file mode 100644 index 000000000..0a603f59f --- /dev/null +++ b/src/utils/adjacentNumber.ts @@ -0,0 +1,9 @@ +// Adjacent floating-point values keep even very narrow windows from collapsing. +export const adjacentNumber = (value: number, direction: number) => { + if (value === 0) return direction * Number.MIN_VALUE; + const view = new DataView(new ArrayBuffer(8)); + view.setFloat64(0, value); + const bits = view.getBigUint64(0); + view.setBigUint64(0, bits + BigInt((value > 0 ? 1 : -1) * direction)); + return view.getFloat64(0); +}; diff --git a/src/utils/colorMapGradient.ts b/src/utils/colorMapGradient.ts new file mode 100644 index 000000000..4cb7025f5 --- /dev/null +++ b/src/utils/colorMapGradient.ts @@ -0,0 +1,76 @@ +import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; +import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps'; + +type Range = readonly [number, number]; +const samples = new WeakMap(); +const clamp = (value: number) => Math.max(0, Math.min(1, value)); + +function getSamples( + preset: NonNullable> +) { + const cached = samples.get(preset); + if (cached) return cached; + const colors = vtkColorTransferFunction.newInstance(); + colors.applyColorMap(preset); + colors.setMappingRange(0, 1); + colors.updateRange(); + // Sample VTK interpolation so HSV and Lab palettes match the rendered image. + const result = Array.from({ length: 256 }, (_, index) => { + const rgb = [0, 0, 0]; + colors.getColor(index / 255, rgb); + return rgb; + }); + colors.delete(); + samples.set(preset, result); + return result; +} + +export function colorMapGradient( + name: string, + domain: Range = [0, 1], + mappingRange: Range = [0, 1], + transparentBackground = false +) { + const preset = vtkColorMaps.getPresetByName(name); + const [low, high] = domain; + const [min, max] = mappingRange; + const width = max - min; + const span = high - low; + if ( + !preset || + ![...domain, ...mappingRange, span, width].every(Number.isFinite) || + span < 0 || + width <= 0 + ) + return undefined; + const colors = getSamples(preset); + const mix = (start: number, end: number, fraction: number) => + start * (1 - fraction) + end * fraction; + // Sampling the window preserves narrow ramps within a much wider image range. + const values = [ + low, + high, + mix(min, max, 0.1), + ...Array.from({ length: colors.length }, (_, index) => + mix(min, max, index / (colors.length - 1)) + ), + ].filter((value) => value >= low && value <= high); + + const stops = [...new Set(values)] + .sort((a, b) => a - b) + .map((value) => { + const fraction = + value <= min ? 0 : value >= max ? 1 : (value - min) / width; + const index = clamp(fraction) * (colors.length - 1); + const first = Math.floor(index); + const second = Math.min(first + 1, colors.length - 1); + const rgb = colors[first].map((channel, component) => + Math.round(mix(channel, colors[second][component], index - first) * 255) + ); + const alpha = transparentBackground ? clamp(fraction * 10) : 1; + const position = span === 0 ? 0 : ((value - low) / span) * 100; + return `rgba(${rgb.join(',')},${alpha}) ${position}%`; + }); + if (stops.length === 1) stops.push(stops[0].replace(' 0%', ' 100%')); + return `linear-gradient(to right, ${stops.join(',')})`; +} diff --git a/src/utils/formatDisplayRange.ts b/src/utils/formatDisplayRange.ts new file mode 100644 index 000000000..527efb5c9 --- /dev/null +++ b/src/utils/formatDisplayRange.ts @@ -0,0 +1,25 @@ +const compactNumber = (value: number) => { + const magnitude = Math.abs(value); + return magnitude !== 0 && (magnitude < 1e-4 || magnitude >= 1e6) + ? value.toExponential() + : String(value); +}; + +export function formatDisplayRange([min, max]: readonly [number, number]) { + const span = max - min; + if (Number.isFinite(span) && span > 0) { + for (let precision = 4; precision <= 17; precision += 1) { + const roundedMin = Number(min.toPrecision(precision)); + const roundedMax = Number(max.toPrecision(precision)); + // Divide by the span so subnormal intervals do not lose their tolerance. + if ( + roundedMin < roundedMax && + Math.abs(roundedMin - min) / span <= 0.001 && + Math.abs(roundedMax - max) / span <= 0.001 + ) { + return [compactNumber(roundedMin), compactNumber(roundedMax)] as const; + } + } + } + return [compactNumber(min), compactNumber(max)] as const; +} diff --git a/src/utils/isValidDisplayRange.ts b/src/utils/isValidDisplayRange.ts new file mode 100644 index 000000000..c48c5b0e5 --- /dev/null +++ b/src/utils/isValidDisplayRange.ts @@ -0,0 +1,7 @@ +export const isValidDisplayRange = ( + range: readonly [number, number] | undefined +): range is readonly [number, number] => + !!range && + range.every(Number.isFinite) && + range[0] < range[1] && + Number.isFinite(range[1] - range[0]); diff --git a/src/vtk/ColorMaps.ts b/src/vtk/ColorMaps.ts index 9ea3e5bb4..669dfc1a3 100644 --- a/src/vtk/ColorMaps.ts +++ b/src/vtk/ColorMaps.ts @@ -1,20 +1,15 @@ import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps'; -import MedicalPresets from '@/src/vtk/MedicalColorPresets.json'; +import MedicalPresets from './MedicalColorPresets.json'; +import DicomPresets from './DicomColorPresets.json'; -function registerPresets(presets: typeof MedicalPresets) { - for (let i = 0; i < presets.length; i += 1) { - vtkColorMaps.addPreset(presets[i]); - } -} - -registerPresets(MedicalPresets); - -const twoHotOpaque = { +MedicalPresets.forEach(vtkColorMaps.addPreset); +vtkColorMaps.addPreset({ ...vtkColorMaps.getPresetByName('2hot'), Name: '2hot-opaque', OpacityPoints: [0, 1], -}; -vtkColorMaps.addPreset(twoHotOpaque); +}); +// DICOM PS3.6 Annex B: https://dicom.nema.org/medical/dicom/current/output/chtml/part06/chapter_B.html +DicomPresets.forEach(vtkColorMaps.addPreset); /* prettier-ignore */ const GroupedPresets = [ diff --git a/src/vtk/DicomColorPresets.json b/src/vtk/DicomColorPresets.json new file mode 100644 index 000000000..de850ddaa --- /dev/null +++ b/src/vtk/DicomColorPresets.json @@ -0,0 +1,1312 @@ +[ + { + "Name": "DICOM Hot Iron", + "ColorSpace": "RGB", + "RGBPoints": [ + 0, 0, 0, 0, + 1, 0.00784313725490196, 0, 0, + 2, 0.01568627450980392, 0, 0, + 3, 0.023529411764705882, 0, 0, + 4, 0.03137254901960784, 0, 0, + 5, 0.0392156862745098, 0, 0, + 6, 0.047058823529411764, 0, 0, + 7, 0.054901960784313725, 0, 0, + 8, 0.06274509803921569, 0, 0, + 9, 0.07058823529411765, 0, 0, + 10, 0.0784313725490196, 0, 0, + 11, 0.08627450980392157, 0, 0, + 12, 0.09411764705882353, 0, 0, + 13, 0.10196078431372549, 0, 0, + 14, 0.10980392156862745, 0, 0, + 15, 0.11764705882352941, 0, 0, + 16, 0.12549019607843137, 0, 0, + 17, 0.13333333333333333, 0, 0, + 18, 0.1411764705882353, 0, 0, + 19, 0.14901960784313725, 0, 0, + 20, 0.1568627450980392, 0, 0, + 21, 0.16470588235294117, 0, 0, + 22, 0.17254901960784313, 0, 0, + 23, 0.1803921568627451, 0, 0, + 24, 0.18823529411764706, 0, 0, + 25, 0.19607843137254902, 0, 0, + 26, 0.20392156862745098, 0, 0, + 27, 0.21176470588235294, 0, 0, + 28, 0.2196078431372549, 0, 0, + 29, 0.22745098039215686, 0, 0, + 30, 0.23529411764705882, 0, 0, + 31, 0.24313725490196078, 0, 0, + 32, 0.25098039215686274, 0, 0, + 33, 0.25882352941176473, 0, 0, + 34, 0.26666666666666666, 0, 0, + 35, 0.27450980392156865, 0, 0, + 36, 0.2823529411764706, 0, 0, + 37, 0.2901960784313726, 0, 0, + 38, 0.2980392156862745, 0, 0, + 39, 0.3058823529411765, 0, 0, + 40, 0.3137254901960784, 0, 0, + 41, 0.3215686274509804, 0, 0, + 42, 0.32941176470588235, 0, 0, + 43, 0.33725490196078434, 0, 0, + 44, 0.34509803921568627, 0, 0, + 45, 0.35294117647058826, 0, 0, + 46, 0.3607843137254902, 0, 0, + 47, 0.3686274509803922, 0, 0, + 48, 0.3764705882352941, 0, 0, + 49, 0.3843137254901961, 0, 0, + 50, 0.39215686274509803, 0, 0, + 51, 0.4, 0, 0, + 52, 0.40784313725490196, 0, 0, + 53, 0.41568627450980394, 0, 0, + 54, 0.4235294117647059, 0, 0, + 55, 0.43137254901960786, 0, 0, + 56, 0.4392156862745098, 0, 0, + 57, 0.4470588235294118, 0, 0, + 58, 0.4549019607843137, 0, 0, + 59, 0.4627450980392157, 0, 0, + 60, 0.47058823529411764, 0, 0, + 61, 0.47843137254901963, 0, 0, + 62, 0.48627450980392156, 0, 0, + 63, 0.49411764705882355, 0, 0, + 64, 0.5019607843137255, 0, 0, + 65, 0.5098039215686274, 0, 0, + 66, 0.5176470588235295, 0, 0, + 67, 0.5254901960784314, 0, 0, + 68, 0.5333333333333333, 0, 0, + 69, 0.5411764705882353, 0, 0, + 70, 0.5490196078431373, 0, 0, + 71, 0.5568627450980392, 0, 0, + 72, 0.5647058823529412, 0, 0, + 73, 0.5725490196078431, 0, 0, + 74, 0.5803921568627451, 0, 0, + 75, 0.5882352941176471, 0, 0, + 76, 0.596078431372549, 0, 0, + 77, 0.6039215686274509, 0, 0, + 78, 0.611764705882353, 0, 0, + 79, 0.6196078431372549, 0, 0, + 80, 0.6274509803921569, 0, 0, + 81, 0.6352941176470588, 0, 0, + 82, 0.6431372549019608, 0, 0, + 83, 0.6509803921568628, 0, 0, + 84, 0.6588235294117647, 0, 0, + 85, 0.6666666666666666, 0, 0, + 86, 0.6745098039215687, 0, 0, + 87, 0.6823529411764706, 0, 0, + 88, 0.6901960784313725, 0, 0, + 89, 0.6980392156862745, 0, 0, + 90, 0.7058823529411765, 0, 0, + 91, 0.7137254901960784, 0, 0, + 92, 0.7215686274509804, 0, 0, + 93, 0.7294117647058823, 0, 0, + 94, 0.7372549019607844, 0, 0, + 95, 0.7450980392156863, 0, 0, + 96, 0.7529411764705882, 0, 0, + 97, 0.7607843137254902, 0, 0, + 98, 0.7686274509803922, 0, 0, + 99, 0.7764705882352941, 0, 0, + 100, 0.7843137254901961, 0, 0, + 101, 0.792156862745098, 0, 0, + 102, 0.8, 0, 0, + 103, 0.807843137254902, 0, 0, + 104, 0.8156862745098039, 0, 0, + 105, 0.8235294117647058, 0, 0, + 106, 0.8313725490196079, 0, 0, + 107, 0.8392156862745098, 0, 0, + 108, 0.8470588235294118, 0, 0, + 109, 0.8549019607843137, 0, 0, + 110, 0.8627450980392157, 0, 0, + 111, 0.8705882352941177, 0, 0, + 112, 0.8784313725490196, 0, 0, + 113, 0.8862745098039215, 0, 0, + 114, 0.8941176470588236, 0, 0, + 115, 0.9019607843137255, 0, 0, + 116, 0.9098039215686274, 0, 0, + 117, 0.9176470588235294, 0, 0, + 118, 0.9254901960784314, 0, 0, + 119, 0.9333333333333333, 0, 0, + 120, 0.9411764705882353, 0, 0, + 121, 0.9490196078431372, 0, 0, + 122, 0.9568627450980393, 0, 0, + 123, 0.9647058823529412, 0, 0, + 124, 0.9725490196078431, 0, 0, + 125, 0.9803921568627451, 0, 0, + 126, 0.9882352941176471, 0, 0, + 127, 0.996078431372549, 0, 0, + 128, 1, 0, 0, + 129, 1, 0.00784313725490196, 0, + 130, 1, 0.01568627450980392, 0, + 131, 1, 0.023529411764705882, 0, + 132, 1, 0.03137254901960784, 0, + 133, 1, 0.0392156862745098, 0, + 134, 1, 0.047058823529411764, 0, + 135, 1, 0.054901960784313725, 0, + 136, 1, 0.06274509803921569, 0, + 137, 1, 0.07058823529411765, 0, + 138, 1, 0.0784313725490196, 0, + 139, 1, 0.08627450980392157, 0, + 140, 1, 0.09411764705882353, 0, + 141, 1, 0.10196078431372549, 0, + 142, 1, 0.10980392156862745, 0, + 143, 1, 0.11764705882352941, 0, + 144, 1, 0.12549019607843137, 0, + 145, 1, 0.13333333333333333, 0, + 146, 1, 0.1411764705882353, 0, + 147, 1, 0.14901960784313725, 0, + 148, 1, 0.1568627450980392, 0, + 149, 1, 0.16470588235294117, 0, + 150, 1, 0.17254901960784313, 0, + 151, 1, 0.1803921568627451, 0, + 152, 1, 0.18823529411764706, 0, + 153, 1, 0.19607843137254902, 0, + 154, 1, 0.20392156862745098, 0, + 155, 1, 0.21176470588235294, 0, + 156, 1, 0.2196078431372549, 0, + 157, 1, 0.22745098039215686, 0, + 158, 1, 0.23529411764705882, 0, + 159, 1, 0.24313725490196078, 0, + 160, 1, 0.25098039215686274, 0, + 161, 1, 0.25882352941176473, 0, + 162, 1, 0.26666666666666666, 0, + 163, 1, 0.27450980392156865, 0, + 164, 1, 0.2823529411764706, 0, + 165, 1, 0.2901960784313726, 0, + 166, 1, 0.2980392156862745, 0, + 167, 1, 0.3058823529411765, 0, + 168, 1, 0.3137254901960784, 0, + 169, 1, 0.3215686274509804, 0, + 170, 1, 0.32941176470588235, 0, + 171, 1, 0.33725490196078434, 0, + 172, 1, 0.34509803921568627, 0, + 173, 1, 0.35294117647058826, 0, + 174, 1, 0.3607843137254902, 0, + 175, 1, 0.3686274509803922, 0, + 176, 1, 0.3764705882352941, 0, + 177, 1, 0.3843137254901961, 0, + 178, 1, 0.39215686274509803, 0, + 179, 1, 0.4, 0, + 180, 1, 0.40784313725490196, 0, + 181, 1, 0.41568627450980394, 0, + 182, 1, 0.4235294117647059, 0, + 183, 1, 0.43137254901960786, 0, + 184, 1, 0.4392156862745098, 0, + 185, 1, 0.4470588235294118, 0, + 186, 1, 0.4549019607843137, 0, + 187, 1, 0.4627450980392157, 0, + 188, 1, 0.47058823529411764, 0, + 189, 1, 0.47843137254901963, 0, + 190, 1, 0.48627450980392156, 0, + 191, 1, 0.49411764705882355, 0, + 192, 1, 0.5019607843137255, 0.01568627450980392, + 193, 1, 0.5098039215686274, 0.03137254901960784, + 194, 1, 0.5176470588235295, 0.047058823529411764, + 195, 1, 0.5254901960784314, 0.06274509803921569, + 196, 1, 0.5333333333333333, 0.0784313725490196, + 197, 1, 0.5411764705882353, 0.09411764705882353, + 198, 1, 0.5490196078431373, 0.10980392156862745, + 199, 1, 0.5568627450980392, 0.12549019607843137, + 200, 1, 0.5647058823529412, 0.1411764705882353, + 201, 1, 0.5725490196078431, 0.1568627450980392, + 202, 1, 0.5803921568627451, 0.17254901960784313, + 203, 1, 0.5882352941176471, 0.18823529411764706, + 204, 1, 0.596078431372549, 0.20392156862745098, + 205, 1, 0.6039215686274509, 0.2196078431372549, + 206, 1, 0.611764705882353, 0.23529411764705882, + 207, 1, 0.6196078431372549, 0.25098039215686274, + 208, 1, 0.6274509803921569, 0.26666666666666666, + 209, 1, 0.6352941176470588, 0.2823529411764706, + 210, 1, 0.6431372549019608, 0.2980392156862745, + 211, 1, 0.6509803921568628, 0.3137254901960784, + 212, 1, 0.6588235294117647, 0.32941176470588235, + 213, 1, 0.6666666666666666, 0.34509803921568627, + 214, 1, 0.6745098039215687, 0.3607843137254902, + 215, 1, 0.6823529411764706, 0.3764705882352941, + 216, 1, 0.6901960784313725, 0.39215686274509803, + 217, 1, 0.6980392156862745, 0.40784313725490196, + 218, 1, 0.7058823529411765, 0.4235294117647059, + 219, 1, 0.7137254901960784, 0.4392156862745098, + 220, 1, 0.7215686274509804, 0.4549019607843137, + 221, 1, 0.7294117647058823, 0.47058823529411764, + 222, 1, 0.7372549019607844, 0.48627450980392156, + 223, 1, 0.7450980392156863, 0.5019607843137255, + 224, 1, 0.7529411764705882, 0.5176470588235295, + 225, 1, 0.7607843137254902, 0.5333333333333333, + 226, 1, 0.7686274509803922, 0.5490196078431373, + 227, 1, 0.7764705882352941, 0.5647058823529412, + 228, 1, 0.7843137254901961, 0.5803921568627451, + 229, 1, 0.792156862745098, 0.596078431372549, + 230, 1, 0.8, 0.611764705882353, + 231, 1, 0.807843137254902, 0.6274509803921569, + 232, 1, 0.8156862745098039, 0.6431372549019608, + 233, 1, 0.8235294117647058, 0.6588235294117647, + 234, 1, 0.8313725490196079, 0.6745098039215687, + 235, 1, 0.8392156862745098, 0.6901960784313725, + 236, 1, 0.8470588235294118, 0.7058823529411765, + 237, 1, 0.8549019607843137, 0.7215686274509804, + 238, 1, 0.8627450980392157, 0.7372549019607844, + 239, 1, 0.8705882352941177, 0.7529411764705882, + 240, 1, 0.8784313725490196, 0.7686274509803922, + 241, 1, 0.8862745098039215, 0.7843137254901961, + 242, 1, 0.8941176470588236, 0.8, + 243, 1, 0.9019607843137255, 0.8156862745098039, + 244, 1, 0.9098039215686274, 0.8313725490196079, + 245, 1, 0.9176470588235294, 0.8470588235294118, + 246, 1, 0.9254901960784314, 0.8627450980392157, + 247, 1, 0.9333333333333333, 0.8784313725490196, + 248, 1, 0.9411764705882353, 0.8941176470588236, + 249, 1, 0.9490196078431372, 0.9098039215686274, + 250, 1, 0.9568627450980393, 0.9254901960784314, + 251, 1, 0.9647058823529412, 0.9411764705882353, + 252, 1, 0.9725490196078431, 0.9568627450980393, + 253, 1, 0.9803921568627451, 0.9725490196078431, + 254, 1, 0.9882352941176471, 0.9882352941176471, + 255, 1, 1, 1 + ] + }, + { + "Name": "DICOM PET", + "ColorSpace": "RGB", + "RGBPoints": [ + 0, 0, 0, 0, + 1, 0, 0.00784313725490196, 0.00392156862745098, + 2, 0, 0.01568627450980392, 0.011764705882352941, + 3, 0, 0.023529411764705882, 0.0196078431372549, + 4, 0, 0.03137254901960784, 0.027450980392156862, + 5, 0, 0.0392156862745098, 0.03529411764705882, + 6, 0, 0.047058823529411764, 0.043137254901960784, + 7, 0, 0.054901960784313725, 0.050980392156862744, + 8, 0, 0.06274509803921569, 0.058823529411764705, + 9, 0, 0.07058823529411765, 0.06666666666666667, + 10, 0, 0.0784313725490196, 0.07450980392156863, + 11, 0, 0.08627450980392157, 0.08235294117647059, + 12, 0, 0.09411764705882353, 0.09019607843137255, + 13, 0, 0.10196078431372549, 0.09803921568627451, + 14, 0, 0.10980392156862745, 0.10588235294117647, + 15, 0, 0.11764705882352941, 0.11372549019607843, + 16, 0, 0.12549019607843137, 0.12156862745098039, + 17, 0, 0.13333333333333333, 0.12941176470588237, + 18, 0, 0.1411764705882353, 0.13725490196078433, + 19, 0, 0.14901960784313725, 0.1450980392156863, + 20, 0, 0.1568627450980392, 0.15294117647058825, + 21, 0, 0.16470588235294117, 0.1607843137254902, + 22, 0, 0.17254901960784313, 0.16862745098039217, + 23, 0, 0.1803921568627451, 0.17647058823529413, + 24, 0, 0.18823529411764706, 0.1843137254901961, + 25, 0, 0.19607843137254902, 0.19215686274509805, + 26, 0, 0.20392156862745098, 0.2, + 27, 0, 0.21176470588235294, 0.20784313725490197, + 28, 0, 0.2196078431372549, 0.21568627450980393, + 29, 0, 0.22745098039215686, 0.2235294117647059, + 30, 0, 0.23529411764705882, 0.23137254901960785, + 31, 0, 0.24313725490196078, 0.23921568627450981, + 32, 0, 0.2549019607843137, 0.24705882352941178, + 33, 0, 0.2627450980392157, 0.2549019607843137, + 34, 0, 0.27058823529411763, 0.2627450980392157, + 35, 0, 0.2784313725490196, 0.27058823529411763, + 36, 0, 0.28627450980392155, 0.2784313725490196, + 37, 0, 0.29411764705882354, 0.28627450980392155, + 38, 0, 0.30196078431372547, 0.29411764705882354, + 39, 0, 0.30980392156862746, 0.30196078431372547, + 40, 0, 0.3176470588235294, 0.30980392156862746, + 41, 0, 0.3254901960784314, 0.3176470588235294, + 42, 0, 0.3333333333333333, 0.3254901960784314, + 43, 0, 0.3411764705882353, 0.3333333333333333, + 44, 0, 0.34901960784313724, 0.3411764705882353, + 45, 0, 0.3568627450980392, 0.34901960784313724, + 46, 0, 0.36470588235294116, 0.3568627450980392, + 47, 0, 0.37254901960784315, 0.36470588235294116, + 48, 0, 0.3803921568627451, 0.37254901960784315, + 49, 0, 0.38823529411764707, 0.3803921568627451, + 50, 0, 0.396078431372549, 0.38823529411764707, + 51, 0, 0.403921568627451, 0.396078431372549, + 52, 0, 0.4117647058823529, 0.403921568627451, + 53, 0, 0.4196078431372549, 0.4117647058823529, + 54, 0, 0.42745098039215684, 0.4196078431372549, + 55, 0, 0.43529411764705883, 0.42745098039215684, + 56, 0, 0.44313725490196076, 0.43529411764705883, + 57, 0, 0.45098039215686275, 0.44313725490196076, + 58, 0, 0.4588235294117647, 0.45098039215686275, + 59, 0, 0.4666666666666667, 0.4588235294117647, + 60, 0, 0.4745098039215686, 0.4666666666666667, + 61, 0, 0.4823529411764706, 0.4745098039215686, + 62, 0, 0.49019607843137253, 0.4823529411764706, + 63, 0, 0.5019607843137255, 0.49019607843137253, + 64, 0.00392156862745098, 0.49411764705882355, 0.4980392156862745, + 65, 0.011764705882352941, 0.48627450980392156, 0.5058823529411764, + 66, 0.0196078431372549, 0.47843137254901963, 0.5137254901960784, + 67, 0.027450980392156862, 0.47058823529411764, 0.5215686274509804, + 68, 0.03529411764705882, 0.4627450980392157, 0.5294117647058824, + 69, 0.043137254901960784, 0.4549019607843137, 0.5372549019607843, + 70, 0.050980392156862744, 0.4470588235294118, 0.5450980392156862, + 71, 0.058823529411764705, 0.4392156862745098, 0.5529411764705883, + 72, 0.06666666666666667, 0.43137254901960786, 0.5607843137254902, + 73, 0.07450980392156863, 0.4235294117647059, 0.5686274509803921, + 74, 0.08235294117647059, 0.41568627450980394, 0.5764705882352941, + 75, 0.09019607843137255, 0.40784313725490196, 0.5843137254901961, + 76, 0.09803921568627451, 0.4, 0.592156862745098, + 77, 0.10588235294117647, 0.39215686274509803, 0.6, + 78, 0.11372549019607843, 0.3843137254901961, 0.6078431372549019, + 79, 0.12156862745098039, 0.3764705882352941, 0.615686274509804, + 80, 0.12941176470588237, 0.3686274509803922, 0.6235294117647059, + 81, 0.13725490196078433, 0.3607843137254902, 0.6313725490196078, + 82, 0.1450980392156863, 0.35294117647058826, 0.6392156862745098, + 83, 0.15294117647058825, 0.34509803921568627, 0.6470588235294118, + 84, 0.1607843137254902, 0.33725490196078434, 0.6549019607843137, + 85, 0.16862745098039217, 0.32941176470588235, 0.6627450980392157, + 86, 0.17647058823529413, 0.3215686274509804, 0.6705882352941176, + 87, 0.1843137254901961, 0.3137254901960784, 0.6784313725490196, + 88, 0.19215686274509805, 0.3058823529411765, 0.6862745098039216, + 89, 0.2, 0.2980392156862745, 0.6941176470588235, + 90, 0.20784313725490197, 0.2901960784313726, 0.7019607843137254, + 91, 0.21568627450980393, 0.2823529411764706, 0.7098039215686275, + 92, 0.2235294117647059, 0.27450980392156865, 0.7176470588235294, + 93, 0.23137254901960785, 0.26666666666666666, 0.7254901960784313, + 94, 0.23921568627450981, 0.25882352941176473, 0.7333333333333333, + 95, 0.24705882352941178, 0.25098039215686274, 0.7411764705882353, + 96, 0.2549019607843137, 0.24705882352941178, 0.7490196078431373, + 97, 0.2627450980392157, 0.23921568627450981, 0.7568627450980392, + 98, 0.27058823529411763, 0.23137254901960785, 0.7647058823529411, + 99, 0.2784313725490196, 0.2235294117647059, 0.7725490196078432, + 100, 0.28627450980392155, 0.21568627450980393, 0.7803921568627451, + 101, 0.29411764705882354, 0.20784313725490197, 0.788235294117647, + 102, 0.30196078431372547, 0.2, 0.796078431372549, + 103, 0.30980392156862746, 0.19215686274509805, 0.803921568627451, + 104, 0.3176470588235294, 0.1843137254901961, 0.8117647058823529, + 105, 0.3254901960784314, 0.17647058823529413, 0.8196078431372549, + 106, 0.3333333333333333, 0.16862745098039217, 0.8274509803921568, + 107, 0.33725490196078434, 0.1607843137254902, 0.8352941176470589, + 108, 0.34509803921568627, 0.15294117647058825, 0.8431372549019608, + 109, 0.35294117647058826, 0.1450980392156863, 0.8509803921568627, + 110, 0.3607843137254902, 0.13725490196078433, 0.8588235294117647, + 111, 0.3686274509803922, 0.12941176470588237, 0.8666666666666667, + 112, 0.3764705882352941, 0.12156862745098039, 0.8745098039215686, + 113, 0.3843137254901961, 0.11372549019607843, 0.8823529411764706, + 114, 0.39215686274509803, 0.10588235294117647, 0.8901960784313725, + 115, 0.4, 0.09803921568627451, 0.8980392156862745, + 116, 0.40784313725490196, 0.09019607843137255, 0.9058823529411765, + 117, 0.41568627450980394, 0.08235294117647059, 0.9137254901960784, + 118, 0.4235294117647059, 0.07450980392156863, 0.9215686274509803, + 119, 0.43137254901960786, 0.06666666666666667, 0.9294117647058824, + 120, 0.4392156862745098, 0.058823529411764705, 0.9372549019607843, + 121, 0.4470588235294118, 0.050980392156862744, 0.9450980392156862, + 122, 0.4549019607843137, 0.043137254901960784, 0.9529411764705882, + 123, 0.4627450980392157, 0.03529411764705882, 0.9607843137254902, + 124, 0.47058823529411764, 0.027450980392156862, 0.9686274509803922, + 125, 0.47843137254901963, 0.0196078431372549, 0.9764705882352941, + 126, 0.48627450980392156, 0.011764705882352941, 0.984313725490196, + 127, 0.49411764705882355, 0.00392156862745098, 0.9921568627450981, + 128, 0.5019607843137255, 0, 1, + 129, 0.5098039215686274, 0.00784313725490196, 0.9882352941176471, + 130, 0.5176470588235295, 0.01568627450980392, 0.9725490196078431, + 131, 0.5254901960784314, 0.023529411764705882, 0.9568627450980393, + 132, 0.5333333333333333, 0.03137254901960784, 0.9411764705882353, + 133, 0.5411764705882353, 0.0392156862745098, 0.9254901960784314, + 134, 0.5490196078431373, 0.047058823529411764, 0.9098039215686274, + 135, 0.5568627450980392, 0.054901960784313725, 0.8941176470588236, + 136, 0.5647058823529412, 0.06274509803921569, 0.8784313725490196, + 137, 0.5725490196078431, 0.07058823529411765, 0.8627450980392157, + 138, 0.5803921568627451, 0.0784313725490196, 0.8470588235294118, + 139, 0.5882352941176471, 0.08627450980392157, 0.8313725490196079, + 140, 0.596078431372549, 0.09411764705882353, 0.8156862745098039, + 141, 0.6039215686274509, 0.10196078431372549, 0.8, + 142, 0.611764705882353, 0.10980392156862745, 0.7843137254901961, + 143, 0.6196078431372549, 0.11764705882352941, 0.7686274509803922, + 144, 0.6274509803921569, 0.12549019607843137, 0.7529411764705882, + 145, 0.6352941176470588, 0.13333333333333333, 0.7372549019607844, + 146, 0.6431372549019608, 0.1411764705882353, 0.7215686274509804, + 147, 0.6509803921568628, 0.14901960784313725, 0.7058823529411765, + 148, 0.6588235294117647, 0.1568627450980392, 0.6901960784313725, + 149, 0.6666666666666666, 0.16470588235294117, 0.6745098039215687, + 150, 0.6705882352941176, 0.17254901960784313, 0.6588235294117647, + 151, 0.6784313725490196, 0.1803921568627451, 0.6431372549019608, + 152, 0.6862745098039216, 0.18823529411764706, 0.6274509803921569, + 153, 0.6941176470588235, 0.19607843137254902, 0.611764705882353, + 154, 0.7019607843137254, 0.20392156862745098, 0.596078431372549, + 155, 0.7098039215686275, 0.21176470588235294, 0.5803921568627451, + 156, 0.7176470588235294, 0.2196078431372549, 0.5647058823529412, + 157, 0.7254901960784313, 0.22745098039215686, 0.5490196078431373, + 158, 0.7333333333333333, 0.23529411764705882, 0.5333333333333333, + 159, 0.7411764705882353, 0.24313725490196078, 0.5176470588235295, + 160, 0.7490196078431373, 0.25098039215686274, 0.5019607843137255, + 161, 0.7568627450980392, 0.25882352941176473, 0.48627450980392156, + 162, 0.7647058823529411, 0.26666666666666666, 0.47058823529411764, + 163, 0.7725490196078432, 0.27450980392156865, 0.4549019607843137, + 164, 0.7803921568627451, 0.2823529411764706, 0.4392156862745098, + 165, 0.788235294117647, 0.2901960784313726, 0.4235294117647059, + 166, 0.796078431372549, 0.2980392156862745, 0.40784313725490196, + 167, 0.803921568627451, 0.3058823529411765, 0.39215686274509803, + 168, 0.8117647058823529, 0.3137254901960784, 0.3764705882352941, + 169, 0.8196078431372549, 0.3215686274509804, 0.3607843137254902, + 170, 0.8274509803921568, 0.32941176470588235, 0.34509803921568627, + 171, 0.8352941176470589, 0.33725490196078434, 0.32941176470588235, + 172, 0.8431372549019608, 0.34509803921568627, 0.3137254901960784, + 173, 0.8509803921568627, 0.35294117647058826, 0.2980392156862745, + 174, 0.8588235294117647, 0.3607843137254902, 0.2823529411764706, + 175, 0.8666666666666667, 0.3686274509803922, 0.26666666666666666, + 176, 0.8745098039215686, 0.3764705882352941, 0.25098039215686274, + 177, 0.8823529411764706, 0.3843137254901961, 0.23529411764705882, + 178, 0.8901960784313725, 0.39215686274509803, 0.2196078431372549, + 179, 0.8980392156862745, 0.4, 0.20392156862745098, + 180, 0.9058823529411765, 0.40784313725490196, 0.18823529411764706, + 181, 0.9137254901960784, 0.41568627450980394, 0.17254901960784313, + 182, 0.9215686274509803, 0.4235294117647059, 0.1568627450980392, + 183, 0.9294117647058824, 0.43137254901960786, 0.1411764705882353, + 184, 0.9372549019607843, 0.4392156862745098, 0.12549019607843137, + 185, 0.9450980392156862, 0.4470588235294118, 0.10980392156862745, + 186, 0.9529411764705882, 0.4549019607843137, 0.09411764705882353, + 187, 0.9607843137254902, 0.4627450980392157, 0.0784313725490196, + 188, 0.9686274509803922, 0.47058823529411764, 0.06274509803921569, + 189, 0.9764705882352941, 0.47843137254901963, 0.047058823529411764, + 190, 0.984313725490196, 0.48627450980392156, 0.03137254901960784, + 191, 0.9921568627450981, 0.49411764705882355, 0.01568627450980392, + 192, 1, 0.5019607843137255, 0, + 193, 1, 0.5098039215686274, 0.01568627450980392, + 194, 1, 0.5176470588235295, 0.03137254901960784, + 195, 1, 0.5254901960784314, 0.047058823529411764, + 196, 1, 0.5333333333333333, 0.06274509803921569, + 197, 1, 0.5411764705882353, 0.0784313725490196, + 198, 1, 0.5490196078431373, 0.09411764705882353, + 199, 1, 0.5568627450980392, 0.10980392156862745, + 200, 1, 0.5647058823529412, 0.12549019607843137, + 201, 1, 0.5725490196078431, 0.1411764705882353, + 202, 1, 0.5803921568627451, 0.1568627450980392, + 203, 1, 0.5882352941176471, 0.17254901960784313, + 204, 1, 0.596078431372549, 0.18823529411764706, + 205, 1, 0.6039215686274509, 0.20392156862745098, + 206, 1, 0.611764705882353, 0.2196078431372549, + 207, 1, 0.6196078431372549, 0.23529411764705882, + 208, 1, 0.6274509803921569, 0.25098039215686274, + 209, 1, 0.6352941176470588, 0.26666666666666666, + 210, 1, 0.6431372549019608, 0.2823529411764706, + 211, 1, 0.6509803921568628, 0.2980392156862745, + 212, 1, 0.6588235294117647, 0.3137254901960784, + 213, 1, 0.6666666666666666, 0.3333333333333333, + 214, 1, 0.6745098039215687, 0.34901960784313724, + 215, 1, 0.6823529411764706, 0.36470588235294116, + 216, 1, 0.6901960784313725, 0.3803921568627451, + 217, 1, 0.6980392156862745, 0.396078431372549, + 218, 1, 0.7058823529411765, 0.4117647058823529, + 219, 1, 0.7137254901960784, 0.42745098039215684, + 220, 1, 0.7215686274509804, 0.44313725490196076, + 221, 1, 0.7294117647058823, 0.4588235294117647, + 222, 1, 0.7372549019607844, 0.4745098039215686, + 223, 1, 0.7450980392156863, 0.49019607843137253, + 224, 1, 0.7529411764705882, 0.5058823529411764, + 225, 1, 0.7607843137254902, 0.5215686274509804, + 226, 1, 0.7686274509803922, 0.5372549019607843, + 227, 1, 0.7764705882352941, 0.5529411764705883, + 228, 1, 0.7843137254901961, 0.5686274509803921, + 229, 1, 0.792156862745098, 0.5843137254901961, + 230, 1, 0.8, 0.6, + 231, 1, 0.807843137254902, 0.615686274509804, + 232, 1, 0.8156862745098039, 0.6313725490196078, + 233, 1, 0.8235294117647058, 0.6470588235294118, + 234, 1, 0.8313725490196079, 0.6666666666666666, + 235, 1, 0.8392156862745098, 0.6823529411764706, + 236, 1, 0.8470588235294118, 0.6980392156862745, + 237, 1, 0.8549019607843137, 0.7137254901960784, + 238, 1, 0.8627450980392157, 0.7294117647058823, + 239, 1, 0.8705882352941177, 0.7450980392156863, + 240, 1, 0.8784313725490196, 0.7607843137254902, + 241, 1, 0.8862745098039215, 0.7764705882352941, + 242, 1, 0.8941176470588236, 0.792156862745098, + 243, 1, 0.9019607843137255, 0.807843137254902, + 244, 1, 0.9098039215686274, 0.8235294117647058, + 245, 1, 0.9176470588235294, 0.8392156862745098, + 246, 1, 0.9254901960784314, 0.8549019607843137, + 247, 1, 0.9333333333333333, 0.8705882352941177, + 248, 1, 0.9411764705882353, 0.8862745098039215, + 249, 1, 0.9490196078431372, 0.9019607843137255, + 250, 1, 0.9568627450980393, 0.9176470588235294, + 251, 1, 0.9647058823529412, 0.9333333333333333, + 252, 1, 0.9725490196078431, 0.9490196078431372, + 253, 1, 0.9803921568627451, 0.9647058823529412, + 254, 1, 0.9882352941176471, 0.9803921568627451, + 255, 1, 1, 1 + ] + }, + { + "Name": "DICOM Hot Metal Blue", + "ColorSpace": "RGB", + "RGBPoints": [ + 0, 0, 0, 0, + 1, 0, 0, 0.00784313725490196, + 2, 0, 0, 0.01568627450980392, + 3, 0, 0, 0.023529411764705882, + 4, 0, 0, 0.03137254901960784, + 5, 0, 0, 0.0392156862745098, + 6, 0, 0, 0.047058823529411764, + 7, 0, 0, 0.054901960784313725, + 8, 0, 0, 0.06274509803921569, + 9, 0, 0, 0.06666666666666667, + 10, 0, 0, 0.07450980392156863, + 11, 0, 0, 0.08235294117647059, + 12, 0, 0, 0.09019607843137255, + 13, 0, 0, 0.09803921568627451, + 14, 0, 0, 0.10588235294117647, + 15, 0, 0, 0.11372549019607843, + 16, 0, 0, 0.12156862745098039, + 17, 0, 0, 0.12941176470588237, + 18, 0, 0, 0.13725490196078433, + 19, 0, 0, 0.1450980392156863, + 20, 0, 0, 0.15294117647058825, + 21, 0, 0, 0.1607843137254902, + 22, 0, 0, 0.16862745098039217, + 23, 0, 0, 0.17647058823529413, + 24, 0, 0, 0.1843137254901961, + 25, 0, 0, 0.19215686274509805, + 26, 0, 0, 0.2, + 27, 0, 0, 0.20784313725490197, + 28, 0, 0, 0.21568627450980393, + 29, 0, 0, 0.2235294117647059, + 30, 0, 0, 0.23137254901960785, + 31, 0, 0, 0.23921568627450981, + 32, 0, 0, 0.24705882352941178, + 33, 0, 0, 0.2549019607843137, + 34, 0, 0, 0.2627450980392157, + 35, 0, 0, 0.27058823529411763, + 36, 0, 0, 0.2784313725490196, + 37, 0, 0, 0.28627450980392155, + 38, 0, 0, 0.29411764705882354, + 39, 0, 0, 0.30196078431372547, + 40, 0, 0, 0.30980392156862746, + 41, 0, 0, 0.3176470588235294, + 42, 0, 0, 0.3254901960784314, + 43, 0, 0, 0.32941176470588235, + 44, 0, 0, 0.33725490196078434, + 45, 0, 0, 0.34509803921568627, + 46, 0, 0, 0.35294117647058826, + 47, 0, 0, 0.3607843137254902, + 48, 0, 0, 0.3686274509803922, + 49, 0, 0, 0.3764705882352941, + 50, 0, 0, 0.3843137254901961, + 51, 0, 0, 0.39215686274509803, + 52, 0, 0, 0.4, + 53, 0, 0, 0.40784313725490196, + 54, 0, 0, 0.41568627450980394, + 55, 0, 0, 0.4235294117647059, + 56, 0, 0, 0.43137254901960786, + 57, 0, 0, 0.4392156862745098, + 58, 0, 0, 0.4470588235294118, + 59, 0, 0, 0.4549019607843137, + 60, 0, 0, 0.4588235294117647, + 61, 0, 0, 0.4666666666666667, + 62, 0, 0, 0.4745098039215686, + 63, 0, 0, 0.4823529411764706, + 64, 0, 0, 0.49019607843137253, + 65, 0, 0, 0.4980392156862745, + 66, 0, 0, 0.5058823529411764, + 67, 0, 0, 0.5137254901960784, + 68, 0, 0, 0.5215686274509804, + 69, 0, 0, 0.5294117647058824, + 70, 0, 0, 0.5372549019607843, + 71, 0, 0, 0.5450980392156862, + 72, 0, 0, 0.5529411764705883, + 73, 0, 0, 0.5607843137254902, + 74, 0, 0, 0.5686274509803921, + 75, 0, 0, 0.5764705882352941, + 76, 0, 0, 0.5843137254901961, + 77, 0, 0, 0.592156862745098, + 78, 0, 0, 0.6, + 79, 0, 0, 0.6078431372549019, + 80, 0, 0, 0.615686274509804, + 81, 0, 0, 0.6235294117647059, + 82, 0, 0, 0.6313725490196078, + 83, 0, 0, 0.6392156862745098, + 84, 0, 0, 0.6470588235294118, + 85, 0, 0, 0.6549019607843137, + 86, 0.011764705882352941, 0, 0.6627450980392157, + 87, 0.023529411764705882, 0, 0.6705882352941176, + 88, 0.03529411764705882, 0, 0.6784313725490196, + 89, 0.047058823529411764, 0, 0.6862745098039216, + 90, 0.058823529411764705, 0, 0.6941176470588235, + 91, 0.07058823529411765, 0, 0.7019607843137254, + 92, 0.08235294117647059, 0, 0.7098039215686275, + 93, 0.09411764705882353, 0, 0.7176470588235294, + 94, 0.10196078431372549, 0, 0.7215686274509804, + 95, 0.11372549019607843, 0, 0.7294117647058823, + 96, 0.12549019607843137, 0, 0.7372549019607844, + 97, 0.13725490196078433, 0, 0.7450980392156863, + 98, 0.14901960784313725, 0, 0.7529411764705882, + 99, 0.1607843137254902, 0, 0.7607843137254902, + 100, 0.17254901960784313, 0, 0.7686274509803922, + 101, 0.1843137254901961, 0, 0.7764705882352941, + 102, 0.19607843137254902, 0, 0.7843137254901961, + 103, 0.20392156862745098, 0, 0.7725490196078432, + 104, 0.21568627450980393, 0, 0.7607843137254902, + 105, 0.2235294117647059, 0, 0.7490196078431373, + 106, 0.23137254901960785, 0, 0.7372549019607844, + 107, 0.24313725490196078, 0, 0.7254901960784313, + 108, 0.25098039215686274, 0, 0.7137254901960784, + 109, 0.25882352941176473, 0, 0.7019607843137254, + 110, 0.27058823529411763, 0, 0.6901960784313725, + 111, 0.2784313725490196, 0, 0.6823529411764706, + 112, 0.2901960784313726, 0, 0.6705882352941176, + 113, 0.2980392156862745, 0, 0.6588235294117647, + 114, 0.3058823529411765, 0, 0.6470588235294118, + 115, 0.3176470588235294, 0, 0.6352941176470588, + 116, 0.3254901960784314, 0, 0.6235294117647059, + 117, 0.3333333333333333, 0, 0.611764705882353, + 118, 0.34509803921568627, 0, 0.6, + 119, 0.35294117647058826, 0, 0.5882352941176471, + 120, 0.36470588235294116, 0.00784313725490196, 0.5647058823529412, + 121, 0.3764705882352941, 0.01568627450980392, 0.5411764705882353, + 122, 0.38823529411764707, 0.023529411764705882, 0.5176470588235295, + 123, 0.4, 0.03137254901960784, 0.49411764705882355, + 124, 0.4117647058823529, 0.03529411764705882, 0.4745098039215686, + 125, 0.4235294117647059, 0.043137254901960784, 0.45098039215686275, + 126, 0.43529411764705883, 0.050980392156862744, 0.42745098039215684, + 127, 0.4470588235294118, 0.058823529411764705, 0.403921568627451, + 128, 0.4549019607843137, 0.06666666666666667, 0.3803921568627451, + 129, 0.4666666666666667, 0.07450980392156863, 0.3568627450980392, + 130, 0.47843137254901963, 0.08235294117647059, 0.3333333333333333, + 131, 0.49019607843137253, 0.09019607843137255, 0.30980392156862746, + 132, 0.5019607843137255, 0.09411764705882353, 0.2901960784313726, + 133, 0.5137254901960784, 0.10196078431372549, 0.26666666666666666, + 134, 0.5254901960784314, 0.10980392156862745, 0.24313725490196078, + 135, 0.5372549019607843, 0.11764705882352941, 0.2196078431372549, + 136, 0.5490196078431373, 0.12549019607843137, 0.19607843137254902, + 137, 0.5607843137254902, 0.13333333333333333, 0.1843137254901961, + 138, 0.5725490196078431, 0.1411764705882353, 0.17254901960784313, + 139, 0.5843137254901961, 0.14901960784313725, 0.1607843137254902, + 140, 0.596078431372549, 0.1568627450980392, 0.14901960784313725, + 141, 0.6078431372549019, 0.1607843137254902, 0.13725490196078433, + 142, 0.6196078431372549, 0.16862745098039217, 0.12549019607843137, + 143, 0.6313725490196078, 0.17647058823529413, 0.11372549019607843, + 144, 0.6431372549019608, 0.1843137254901961, 0.10196078431372549, + 145, 0.6509803921568628, 0.19215686274509805, 0.09411764705882353, + 146, 0.6627450980392157, 0.2, 0.08235294117647059, + 147, 0.6745098039215687, 0.20784313725490197, 0.07058823529411765, + 148, 0.6862745098039216, 0.21568627450980393, 0.058823529411764705, + 149, 0.6980392156862745, 0.2196078431372549, 0.047058823529411764, + 150, 0.7098039215686275, 0.22745098039215686, 0.03529411764705882, + 151, 0.7215686274509804, 0.23529411764705882, 0.023529411764705882, + 152, 0.7333333333333333, 0.24313725490196078, 0.011764705882352941, + 153, 0.7450980392156863, 0.25098039215686274, 0, + 154, 0.7607843137254902, 0.25882352941176473, 0, + 155, 0.7764705882352941, 0.26666666666666666, 0, + 156, 0.788235294117647, 0.27450980392156865, 0, + 157, 0.803921568627451, 0.2823529411764706, 0, + 158, 0.8196078431372549, 0.28627450980392155, 0, + 159, 0.8352941176470589, 0.29411764705882354, 0, + 160, 0.8509803921568627, 0.30196078431372547, 0, + 161, 0.8666666666666667, 0.30980392156862746, 0, + 162, 0.8784313725490196, 0.3176470588235294, 0, + 163, 0.8941176470588236, 0.3254901960784314, 0, + 164, 0.9098039215686274, 0.3333333333333333, 0, + 165, 0.9254901960784314, 0.3411764705882353, 0, + 166, 0.9411764705882353, 0.34509803921568627, 0, + 167, 0.9568627450980393, 0.35294117647058826, 0, + 168, 0.9686274509803922, 0.3607843137254902, 0, + 169, 0.984313725490196, 0.3686274509803922, 0, + 170, 1, 0.3764705882352941, 0, + 171, 1, 0.3843137254901961, 0.011764705882352941, + 172, 1, 0.39215686274509803, 0.023529411764705882, + 173, 1, 0.4, 0.03529411764705882, + 174, 1, 0.40784313725490196, 0.047058823529411764, + 175, 1, 0.4117647058823529, 0.058823529411764705, + 176, 1, 0.4196078431372549, 0.07058823529411765, + 177, 1, 0.42745098039215684, 0.08235294117647059, + 178, 1, 0.43529411764705883, 0.09411764705882353, + 179, 1, 0.44313725490196076, 0.10196078431372549, + 180, 1, 0.45098039215686275, 0.11372549019607843, + 181, 1, 0.4588235294117647, 0.12549019607843137, + 182, 1, 0.4666666666666667, 0.13725490196078433, + 183, 1, 0.47058823529411764, 0.14901960784313725, + 184, 1, 0.47843137254901963, 0.1607843137254902, + 185, 1, 0.48627450980392156, 0.17254901960784313, + 186, 1, 0.49411764705882355, 0.1843137254901961, + 187, 1, 0.5019607843137255, 0.19607843137254902, + 188, 1, 0.5098039215686274, 0.20784313725490197, + 189, 1, 0.5176470588235295, 0.2196078431372549, + 190, 1, 0.5254901960784314, 0.23137254901960785, + 191, 1, 0.5333333333333333, 0.24313725490196078, + 192, 1, 0.5372549019607843, 0.2549019607843137, + 193, 1, 0.5450980392156862, 0.26666666666666666, + 194, 1, 0.5529411764705883, 0.2784313725490196, + 195, 1, 0.5607843137254902, 0.2901960784313726, + 196, 1, 0.5686274509803921, 0.2980392156862745, + 197, 1, 0.5764705882352941, 0.30980392156862746, + 198, 1, 0.5843137254901961, 0.3215686274509804, + 199, 1, 0.592156862745098, 0.3333333333333333, + 200, 1, 0.596078431372549, 0.34509803921568627, + 201, 1, 0.6039215686274509, 0.3568627450980392, + 202, 1, 0.611764705882353, 0.3686274509803922, + 203, 1, 0.6196078431372549, 0.3803921568627451, + 204, 1, 0.6274509803921569, 0.39215686274509803, + 205, 1, 0.6352941176470588, 0.403921568627451, + 206, 1, 0.6431372549019608, 0.41568627450980394, + 207, 1, 0.6509803921568628, 0.42745098039215684, + 208, 1, 0.6588235294117647, 0.4392156862745098, + 209, 1, 0.6627450980392157, 0.45098039215686275, + 210, 1, 0.6705882352941176, 0.4627450980392157, + 211, 1, 0.6784313725490196, 0.4745098039215686, + 212, 1, 0.6862745098039216, 0.48627450980392156, + 213, 1, 0.6941176470588235, 0.49411764705882355, + 214, 1, 0.7019607843137254, 0.5058823529411764, + 215, 1, 0.7098039215686275, 0.5176470588235295, + 216, 1, 0.7176470588235294, 0.5294117647058824, + 217, 1, 0.7215686274509804, 0.5411764705882353, + 218, 1, 0.7294117647058823, 0.5529411764705883, + 219, 1, 0.7372549019607844, 0.5647058823529412, + 220, 1, 0.7450980392156863, 0.5764705882352941, + 221, 1, 0.7529411764705882, 0.5882352941176471, + 222, 1, 0.7607843137254902, 0.6, + 223, 1, 0.7686274509803922, 0.611764705882353, + 224, 1, 0.7764705882352941, 0.6235294117647059, + 225, 1, 0.7843137254901961, 0.6352941176470588, + 226, 1, 0.788235294117647, 0.6470588235294118, + 227, 1, 0.796078431372549, 0.6588235294117647, + 228, 1, 0.803921568627451, 0.6705882352941176, + 229, 1, 0.8117647058823529, 0.6823529411764706, + 230, 1, 0.8196078431372549, 0.6901960784313725, + 231, 1, 0.8274509803921568, 0.7019607843137254, + 232, 1, 0.8352941176470589, 0.7137254901960784, + 233, 1, 0.8431372549019608, 0.7254901960784313, + 234, 1, 0.8470588235294118, 0.7372549019607844, + 235, 1, 0.8549019607843137, 0.7490196078431373, + 236, 1, 0.8627450980392157, 0.7607843137254902, + 237, 1, 0.8705882352941177, 0.7725490196078432, + 238, 1, 0.8784313725490196, 0.7843137254901961, + 239, 1, 0.8862745098039215, 0.796078431372549, + 240, 1, 0.8941176470588236, 0.807843137254902, + 241, 1, 0.8980392156862745, 0.8235294117647058, + 242, 1, 0.9058823529411765, 0.8352941176470589, + 243, 1, 0.9137254901960784, 0.8470588235294118, + 244, 1, 0.9215686274509803, 0.8588235294117647, + 245, 1, 0.9294117647058824, 0.8745098039215686, + 246, 1, 0.9372549019607843, 0.8862745098039215, + 247, 1, 0.9411764705882353, 0.8980392156862745, + 248, 1, 0.9490196078431372, 0.9098039215686274, + 249, 1, 0.9568627450980393, 0.9254901960784314, + 250, 1, 0.9647058823529412, 0.9372549019607843, + 251, 1, 0.9725490196078431, 0.9490196078431372, + 252, 1, 0.9803921568627451, 0.9607843137254902, + 253, 1, 0.984313725490196, 0.9764705882352941, + 254, 1, 0.9921568627450981, 0.9882352941176471, + 255, 1, 1, 1 + ] + }, + { + "Name": "DICOM Spring", + "ColorSpace": "RGB", + "RGBPoints": [ + 0, 1, 0, 1, + 1, 1, 0.00392156862745098, 0.996078431372549, + 2, 1, 0.00784313725490196, 0.9921568627450981, + 3, 1, 0.011764705882352941, 0.9882352941176471, + 4, 1, 0.01568627450980392, 0.984313725490196, + 5, 1, 0.0196078431372549, 0.9803921568627451, + 6, 1, 0.023529411764705882, 0.9764705882352941, + 7, 1, 0.027450980392156862, 0.9725490196078431, + 8, 1, 0.03137254901960784, 0.9686274509803922, + 9, 1, 0.03529411764705882, 0.9647058823529412, + 10, 1, 0.0392156862745098, 0.9607843137254902, + 11, 1, 0.043137254901960784, 0.9568627450980393, + 12, 1, 0.047058823529411764, 0.9529411764705882, + 13, 1, 0.050980392156862744, 0.9490196078431372, + 14, 1, 0.054901960784313725, 0.9450980392156862, + 15, 1, 0.058823529411764705, 0.9411764705882353, + 16, 1, 0.06274509803921569, 0.9372549019607843, + 17, 1, 0.06666666666666667, 0.9333333333333333, + 18, 1, 0.07058823529411765, 0.9294117647058824, + 19, 1, 0.07450980392156863, 0.9254901960784314, + 20, 1, 0.0784313725490196, 0.9215686274509803, + 21, 1, 0.08235294117647059, 0.9176470588235294, + 22, 1, 0.08627450980392157, 0.9137254901960784, + 23, 1, 0.09019607843137255, 0.9098039215686274, + 24, 1, 0.09411764705882353, 0.9058823529411765, + 25, 1, 0.09803921568627451, 0.9019607843137255, + 26, 1, 0.10196078431372549, 0.8980392156862745, + 27, 1, 0.10588235294117647, 0.8941176470588236, + 28, 1, 0.10980392156862745, 0.8901960784313725, + 29, 1, 0.11372549019607843, 0.8862745098039215, + 30, 1, 0.11764705882352941, 0.8823529411764706, + 31, 1, 0.12156862745098039, 0.8784313725490196, + 32, 1, 0.12549019607843137, 0.8745098039215686, + 33, 1, 0.12941176470588237, 0.8705882352941177, + 34, 1, 0.13333333333333333, 0.8666666666666667, + 35, 1, 0.13725490196078433, 0.8627450980392157, + 36, 1, 0.1411764705882353, 0.8588235294117647, + 37, 1, 0.1450980392156863, 0.8549019607843137, + 38, 1, 0.14901960784313725, 0.8509803921568627, + 39, 1, 0.15294117647058825, 0.8470588235294118, + 40, 1, 0.1568627450980392, 0.8431372549019608, + 41, 1, 0.1607843137254902, 0.8392156862745098, + 42, 1, 0.16470588235294117, 0.8352941176470589, + 43, 1, 0.16862745098039217, 0.8313725490196079, + 44, 1, 0.17254901960784313, 0.8274509803921568, + 45, 1, 0.17647058823529413, 0.8235294117647058, + 46, 1, 0.1803921568627451, 0.8196078431372549, + 47, 1, 0.1843137254901961, 0.8156862745098039, + 48, 1, 0.18823529411764706, 0.8117647058823529, + 49, 1, 0.19215686274509805, 0.807843137254902, + 50, 1, 0.19607843137254902, 0.803921568627451, + 51, 1, 0.2, 0.8, + 52, 1, 0.20392156862745098, 0.796078431372549, + 53, 1, 0.20784313725490197, 0.792156862745098, + 54, 1, 0.21176470588235294, 0.788235294117647, + 55, 1, 0.21568627450980393, 0.7843137254901961, + 56, 1, 0.2196078431372549, 0.7803921568627451, + 57, 1, 0.2235294117647059, 0.7764705882352941, + 58, 1, 0.22745098039215686, 0.7725490196078432, + 59, 1, 0.23137254901960785, 0.7686274509803922, + 60, 1, 0.23529411764705882, 0.7647058823529411, + 61, 1, 0.23921568627450981, 0.7607843137254902, + 62, 1, 0.24313725490196078, 0.7568627450980392, + 63, 1, 0.24705882352941178, 0.7529411764705882, + 64, 1, 0.25098039215686274, 0.7490196078431373, + 65, 1, 0.2549019607843137, 0.7450980392156863, + 66, 1, 0.25882352941176473, 0.7411764705882353, + 67, 1, 0.2627450980392157, 0.7372549019607844, + 68, 1, 0.26666666666666666, 0.7333333333333333, + 69, 1, 0.27058823529411763, 0.7294117647058823, + 70, 1, 0.27450980392156865, 0.7254901960784313, + 71, 1, 0.2784313725490196, 0.7215686274509804, + 72, 1, 0.2823529411764706, 0.7176470588235294, + 73, 1, 0.28627450980392155, 0.7137254901960784, + 74, 1, 0.2901960784313726, 0.7098039215686275, + 75, 1, 0.29411764705882354, 0.7058823529411765, + 76, 1, 0.2980392156862745, 0.7019607843137254, + 77, 1, 0.30196078431372547, 0.6980392156862745, + 78, 1, 0.3058823529411765, 0.6941176470588235, + 79, 1, 0.30980392156862746, 0.6901960784313725, + 80, 1, 0.3137254901960784, 0.6862745098039216, + 81, 1, 0.3176470588235294, 0.6823529411764706, + 82, 1, 0.3215686274509804, 0.6784313725490196, + 83, 1, 0.3254901960784314, 0.6745098039215687, + 84, 1, 0.32941176470588235, 0.6705882352941176, + 85, 1, 0.3333333333333333, 0.6666666666666666, + 86, 1, 0.33725490196078434, 0.6627450980392157, + 87, 1, 0.3411764705882353, 0.6588235294117647, + 88, 1, 0.34509803921568627, 0.6549019607843137, + 89, 1, 0.34901960784313724, 0.6509803921568628, + 90, 1, 0.35294117647058826, 0.6470588235294118, + 91, 1, 0.3568627450980392, 0.6431372549019608, + 92, 1, 0.3607843137254902, 0.6392156862745098, + 93, 1, 0.36470588235294116, 0.6352941176470588, + 94, 1, 0.3686274509803922, 0.6313725490196078, + 95, 1, 0.37254901960784315, 0.6274509803921569, + 96, 1, 0.3764705882352941, 0.6235294117647059, + 97, 1, 0.3803921568627451, 0.6196078431372549, + 98, 1, 0.3843137254901961, 0.615686274509804, + 99, 1, 0.38823529411764707, 0.611764705882353, + 100, 1, 0.39215686274509803, 0.6078431372549019, + 101, 1, 0.396078431372549, 0.6039215686274509, + 102, 1, 0.4, 0.6, + 103, 1, 0.403921568627451, 0.596078431372549, + 104, 1, 0.40784313725490196, 0.592156862745098, + 105, 1, 0.4117647058823529, 0.5882352941176471, + 106, 1, 0.41568627450980394, 0.5843137254901961, + 107, 1, 0.4196078431372549, 0.5803921568627451, + 108, 1, 0.4235294117647059, 0.5764705882352941, + 109, 1, 0.42745098039215684, 0.5725490196078431, + 110, 1, 0.43137254901960786, 0.5686274509803921, + 111, 1, 0.43529411764705883, 0.5647058823529412, + 112, 1, 0.4392156862745098, 0.5607843137254902, + 113, 1, 0.44313725490196076, 0.5568627450980392, + 114, 1, 0.4470588235294118, 0.5529411764705883, + 115, 1, 0.45098039215686275, 0.5490196078431373, + 116, 1, 0.4549019607843137, 0.5450980392156862, + 117, 1, 0.4588235294117647, 0.5411764705882353, + 118, 1, 0.4627450980392157, 0.5372549019607843, + 119, 1, 0.4666666666666667, 0.5333333333333333, + 120, 1, 0.47058823529411764, 0.5294117647058824, + 121, 1, 0.4745098039215686, 0.5254901960784314, + 122, 1, 0.47843137254901963, 0.5215686274509804, + 123, 1, 0.4823529411764706, 0.5176470588235295, + 124, 1, 0.48627450980392156, 0.5137254901960784, + 125, 1, 0.49019607843137253, 0.5098039215686274, + 126, 1, 0.49411764705882355, 0.5058823529411764, + 127, 1, 0.4980392156862745, 0.5019607843137255, + 128, 1, 0.5019607843137255, 0.4980392156862745, + 129, 1, 0.5058823529411764, 0.49411764705882355, + 130, 1, 0.5098039215686274, 0.49019607843137253, + 131, 1, 0.5137254901960784, 0.48627450980392156, + 132, 1, 0.5176470588235295, 0.4823529411764706, + 133, 1, 0.5215686274509804, 0.47843137254901963, + 134, 1, 0.5254901960784314, 0.4745098039215686, + 135, 1, 0.5294117647058824, 0.47058823529411764, + 136, 1, 0.5333333333333333, 0.4666666666666667, + 137, 1, 0.5372549019607843, 0.4627450980392157, + 138, 1, 0.5411764705882353, 0.4588235294117647, + 139, 1, 0.5450980392156862, 0.4549019607843137, + 140, 1, 0.5490196078431373, 0.45098039215686275, + 141, 1, 0.5529411764705883, 0.4470588235294118, + 142, 1, 0.5568627450980392, 0.44313725490196076, + 143, 1, 0.5607843137254902, 0.4392156862745098, + 144, 1, 0.5647058823529412, 0.43529411764705883, + 145, 1, 0.5686274509803921, 0.43137254901960786, + 146, 1, 0.5725490196078431, 0.42745098039215684, + 147, 1, 0.5764705882352941, 0.4235294117647059, + 148, 1, 0.5803921568627451, 0.4196078431372549, + 149, 1, 0.5843137254901961, 0.41568627450980394, + 150, 1, 0.5882352941176471, 0.4117647058823529, + 151, 1, 0.592156862745098, 0.40784313725490196, + 152, 1, 0.596078431372549, 0.403921568627451, + 153, 1, 0.6, 0.4, + 154, 1, 0.6039215686274509, 0.396078431372549, + 155, 1, 0.6078431372549019, 0.39215686274509803, + 156, 1, 0.611764705882353, 0.38823529411764707, + 157, 1, 0.615686274509804, 0.3843137254901961, + 158, 1, 0.6196078431372549, 0.3803921568627451, + 159, 1, 0.6235294117647059, 0.3764705882352941, + 160, 1, 0.6274509803921569, 0.37254901960784315, + 161, 1, 0.6313725490196078, 0.3686274509803922, + 162, 1, 0.6352941176470588, 0.36470588235294116, + 163, 1, 0.6392156862745098, 0.3607843137254902, + 164, 1, 0.6431372549019608, 0.3568627450980392, + 165, 1, 0.6470588235294118, 0.35294117647058826, + 166, 1, 0.6509803921568628, 0.34901960784313724, + 167, 1, 0.6549019607843137, 0.34509803921568627, + 168, 1, 0.6588235294117647, 0.3411764705882353, + 169, 1, 0.6627450980392157, 0.33725490196078434, + 170, 1, 0.6666666666666666, 0.3333333333333333, + 171, 1, 0.6705882352941176, 0.32941176470588235, + 172, 1, 0.6745098039215687, 0.3254901960784314, + 173, 1, 0.6784313725490196, 0.3215686274509804, + 174, 1, 0.6823529411764706, 0.3176470588235294, + 175, 1, 0.6862745098039216, 0.3137254901960784, + 176, 1, 0.6901960784313725, 0.30980392156862746, + 177, 1, 0.6941176470588235, 0.3058823529411765, + 178, 1, 0.6980392156862745, 0.30196078431372547, + 179, 1, 0.7019607843137254, 0.2980392156862745, + 180, 1, 0.7058823529411765, 0.29411764705882354, + 181, 1, 0.7098039215686275, 0.2901960784313726, + 182, 1, 0.7137254901960784, 0.28627450980392155, + 183, 1, 0.7176470588235294, 0.2823529411764706, + 184, 1, 0.7215686274509804, 0.2784313725490196, + 185, 1, 0.7254901960784313, 0.27450980392156865, + 186, 1, 0.7294117647058823, 0.27058823529411763, + 187, 1, 0.7333333333333333, 0.26666666666666666, + 188, 1, 0.7372549019607844, 0.2627450980392157, + 189, 1, 0.7411764705882353, 0.25882352941176473, + 190, 1, 0.7450980392156863, 0.2549019607843137, + 191, 1, 0.7490196078431373, 0.25098039215686274, + 192, 1, 0.7529411764705882, 0.24705882352941178, + 193, 1, 0.7568627450980392, 0.24313725490196078, + 194, 1, 0.7607843137254902, 0.23921568627450981, + 195, 1, 0.7647058823529411, 0.23529411764705882, + 196, 1, 0.7686274509803922, 0.23137254901960785, + 197, 1, 0.7725490196078432, 0.22745098039215686, + 198, 1, 0.7764705882352941, 0.2235294117647059, + 199, 1, 0.7803921568627451, 0.2196078431372549, + 200, 1, 0.7843137254901961, 0.21568627450980393, + 201, 1, 0.788235294117647, 0.21176470588235294, + 202, 1, 0.792156862745098, 0.20784313725490197, + 203, 1, 0.796078431372549, 0.20392156862745098, + 204, 1, 0.8, 0.2, + 205, 1, 0.803921568627451, 0.19607843137254902, + 206, 1, 0.807843137254902, 0.19215686274509805, + 207, 1, 0.8117647058823529, 0.18823529411764706, + 208, 1, 0.8156862745098039, 0.1843137254901961, + 209, 1, 0.8196078431372549, 0.1803921568627451, + 210, 1, 0.8235294117647058, 0.17647058823529413, + 211, 1, 0.8274509803921568, 0.17254901960784313, + 212, 1, 0.8313725490196079, 0.16862745098039217, + 213, 1, 0.8352941176470589, 0.16470588235294117, + 214, 1, 0.8392156862745098, 0.1607843137254902, + 215, 1, 0.8431372549019608, 0.1568627450980392, + 216, 1, 0.8470588235294118, 0.15294117647058825, + 217, 1, 0.8509803921568627, 0.14901960784313725, + 218, 1, 0.8549019607843137, 0.1450980392156863, + 219, 1, 0.8588235294117647, 0.1411764705882353, + 220, 1, 0.8627450980392157, 0.13725490196078433, + 221, 1, 0.8666666666666667, 0.13333333333333333, + 222, 1, 0.8705882352941177, 0.12941176470588237, + 223, 1, 0.8745098039215686, 0.12549019607843137, + 224, 1, 0.8784313725490196, 0.12156862745098039, + 225, 1, 0.8823529411764706, 0.11764705882352941, + 226, 1, 0.8862745098039215, 0.11372549019607843, + 227, 1, 0.8901960784313725, 0.10980392156862745, + 228, 1, 0.8941176470588236, 0.10588235294117647, + 229, 1, 0.8980392156862745, 0.10196078431372549, + 230, 1, 0.9019607843137255, 0.09803921568627451, + 231, 1, 0.9058823529411765, 0.09411764705882353, + 232, 1, 0.9098039215686274, 0.09019607843137255, + 233, 1, 0.9137254901960784, 0.08627450980392157, + 234, 1, 0.9176470588235294, 0.08235294117647059, + 235, 1, 0.9215686274509803, 0.0784313725490196, + 236, 1, 0.9254901960784314, 0.07450980392156863, + 237, 1, 0.9294117647058824, 0.07058823529411765, + 238, 1, 0.9333333333333333, 0.06666666666666667, + 239, 1, 0.9372549019607843, 0.06274509803921569, + 240, 1, 0.9411764705882353, 0.058823529411764705, + 241, 1, 0.9450980392156862, 0.054901960784313725, + 242, 1, 0.9490196078431372, 0.050980392156862744, + 243, 1, 0.9529411764705882, 0.047058823529411764, + 244, 1, 0.9568627450980393, 0.043137254901960784, + 245, 1, 0.9607843137254902, 0.0392156862745098, + 246, 1, 0.9647058823529412, 0.03529411764705882, + 247, 1, 0.9686274509803922, 0.03137254901960784, + 248, 1, 0.9725490196078431, 0.027450980392156862, + 249, 1, 0.9764705882352941, 0.023529411764705882, + 250, 1, 0.9803921568627451, 0.0196078431372549, + 251, 1, 0.984313725490196, 0.01568627450980392, + 252, 1, 0.9882352941176471, 0.011764705882352941, + 253, 1, 0.9921568627450981, 0.00784313725490196, + 254, 1, 0.996078431372549, 0.00392156862745098, + 255, 1, 1, 0 + ] + }, + { + "Name": "DICOM Winter", + "ColorSpace": "RGB", + "RGBPoints": [ + 0, 0, 0, 1, + 1, 0, 0.00392156862745098, 1, + 2, 0, 0.00784313725490196, 0.996078431372549, + 3, 0, 0.011764705882352941, 0.996078431372549, + 4, 0, 0.01568627450980392, 0.9921568627450981, + 5, 0, 0.0196078431372549, 0.9921568627450981, + 6, 0, 0.023529411764705882, 0.9882352941176471, + 7, 0, 0.027450980392156862, 0.9882352941176471, + 8, 0, 0.03137254901960784, 0.984313725490196, + 9, 0, 0.03529411764705882, 0.984313725490196, + 10, 0, 0.0392156862745098, 0.9803921568627451, + 11, 0, 0.043137254901960784, 0.9803921568627451, + 12, 0, 0.047058823529411764, 0.9764705882352941, + 13, 0, 0.050980392156862744, 0.9764705882352941, + 14, 0, 0.054901960784313725, 0.9725490196078431, + 15, 0, 0.058823529411764705, 0.9725490196078431, + 16, 0, 0.06274509803921569, 0.9686274509803922, + 17, 0, 0.06666666666666667, 0.9686274509803922, + 18, 0, 0.07058823529411765, 0.9647058823529412, + 19, 0, 0.07450980392156863, 0.9647058823529412, + 20, 0, 0.0784313725490196, 0.9607843137254902, + 21, 0, 0.08235294117647059, 0.9607843137254902, + 22, 0, 0.08627450980392157, 0.9568627450980393, + 23, 0, 0.09019607843137255, 0.9568627450980393, + 24, 0, 0.09411764705882353, 0.9529411764705882, + 25, 0, 0.09803921568627451, 0.9529411764705882, + 26, 0, 0.10196078431372549, 0.9490196078431372, + 27, 0, 0.10588235294117647, 0.9490196078431372, + 28, 0, 0.10980392156862745, 0.9450980392156862, + 29, 0, 0.11372549019607843, 0.9450980392156862, + 30, 0, 0.11764705882352941, 0.9411764705882353, + 31, 0, 0.12156862745098039, 0.9411764705882353, + 32, 0, 0.12549019607843137, 0.9372549019607843, + 33, 0, 0.12941176470588237, 0.9372549019607843, + 34, 0, 0.13333333333333333, 0.9333333333333333, + 35, 0, 0.13725490196078433, 0.9333333333333333, + 36, 0, 0.1411764705882353, 0.9294117647058824, + 37, 0, 0.1450980392156863, 0.9294117647058824, + 38, 0, 0.14901960784313725, 0.9254901960784314, + 39, 0, 0.15294117647058825, 0.9254901960784314, + 40, 0, 0.1568627450980392, 0.9215686274509803, + 41, 0, 0.1607843137254902, 0.9215686274509803, + 42, 0, 0.16470588235294117, 0.9176470588235294, + 43, 0, 0.16862745098039217, 0.9176470588235294, + 44, 0, 0.17254901960784313, 0.9137254901960784, + 45, 0, 0.17647058823529413, 0.9137254901960784, + 46, 0, 0.1803921568627451, 0.9098039215686274, + 47, 0, 0.1843137254901961, 0.9098039215686274, + 48, 0, 0.18823529411764706, 0.9058823529411765, + 49, 0, 0.19215686274509805, 0.9058823529411765, + 50, 0, 0.19607843137254902, 0.9019607843137255, + 51, 0, 0.2, 0.9019607843137255, + 52, 0, 0.20392156862745098, 0.8980392156862745, + 53, 0, 0.20784313725490197, 0.8980392156862745, + 54, 0, 0.21176470588235294, 0.8941176470588236, + 55, 0, 0.21568627450980393, 0.8941176470588236, + 56, 0, 0.2196078431372549, 0.8901960784313725, + 57, 0, 0.2235294117647059, 0.8901960784313725, + 58, 0, 0.22745098039215686, 0.8862745098039215, + 59, 0, 0.23137254901960785, 0.8862745098039215, + 60, 0, 0.23529411764705882, 0.8823529411764706, + 61, 0, 0.23921568627450981, 0.8823529411764706, + 62, 0, 0.24313725490196078, 0.8784313725490196, + 63, 0, 0.24705882352941178, 0.8784313725490196, + 64, 0, 0.25098039215686274, 0.8745098039215686, + 65, 0, 0.2549019607843137, 0.8745098039215686, + 66, 0, 0.25882352941176473, 0.8705882352941177, + 67, 0, 0.2627450980392157, 0.8705882352941177, + 68, 0, 0.26666666666666666, 0.8666666666666667, + 69, 0, 0.27058823529411763, 0.8666666666666667, + 70, 0, 0.27450980392156865, 0.8627450980392157, + 71, 0, 0.2784313725490196, 0.8627450980392157, + 72, 0, 0.2823529411764706, 0.8588235294117647, + 73, 0, 0.28627450980392155, 0.8588235294117647, + 74, 0, 0.2901960784313726, 0.8549019607843137, + 75, 0, 0.29411764705882354, 0.8549019607843137, + 76, 0, 0.2980392156862745, 0.8509803921568627, + 77, 0, 0.30196078431372547, 0.8509803921568627, + 78, 0, 0.3058823529411765, 0.8470588235294118, + 79, 0, 0.30980392156862746, 0.8470588235294118, + 80, 0, 0.3137254901960784, 0.8431372549019608, + 81, 0, 0.3176470588235294, 0.8431372549019608, + 82, 0, 0.3215686274509804, 0.8392156862745098, + 83, 0, 0.3254901960784314, 0.8392156862745098, + 84, 0, 0.32941176470588235, 0.8352941176470589, + 85, 0, 0.3333333333333333, 0.8352941176470589, + 86, 0, 0.33725490196078434, 0.8313725490196079, + 87, 0, 0.3411764705882353, 0.8313725490196079, + 88, 0, 0.34509803921568627, 0.8274509803921568, + 89, 0, 0.34901960784313724, 0.8274509803921568, + 90, 0, 0.35294117647058826, 0.8235294117647058, + 91, 0, 0.3568627450980392, 0.8235294117647058, + 92, 0, 0.3607843137254902, 0.8196078431372549, + 93, 0, 0.36470588235294116, 0.8196078431372549, + 94, 0, 0.3686274509803922, 0.8156862745098039, + 95, 0, 0.37254901960784315, 0.8156862745098039, + 96, 0, 0.3764705882352941, 0.8117647058823529, + 97, 0, 0.3803921568627451, 0.8117647058823529, + 98, 0, 0.3843137254901961, 0.807843137254902, + 99, 0, 0.38823529411764707, 0.807843137254902, + 100, 0, 0.39215686274509803, 0.803921568627451, + 101, 0, 0.396078431372549, 0.803921568627451, + 102, 0, 0.4, 0.8, + 103, 0, 0.403921568627451, 0.8, + 104, 0, 0.40784313725490196, 0.796078431372549, + 105, 0, 0.4117647058823529, 0.796078431372549, + 106, 0, 0.41568627450980394, 0.792156862745098, + 107, 0, 0.4196078431372549, 0.792156862745098, + 108, 0, 0.4235294117647059, 0.788235294117647, + 109, 0, 0.42745098039215684, 0.788235294117647, + 110, 0, 0.43137254901960786, 0.7843137254901961, + 111, 0, 0.43529411764705883, 0.7843137254901961, + 112, 0, 0.4392156862745098, 0.7803921568627451, + 113, 0, 0.44313725490196076, 0.7803921568627451, + 114, 0, 0.4470588235294118, 0.7764705882352941, + 115, 0, 0.45098039215686275, 0.7764705882352941, + 116, 0, 0.4549019607843137, 0.7725490196078432, + 117, 0, 0.4588235294117647, 0.7725490196078432, + 118, 0, 0.4627450980392157, 0.7686274509803922, + 119, 0, 0.4666666666666667, 0.7686274509803922, + 120, 0, 0.47058823529411764, 0.7647058823529411, + 121, 0, 0.4745098039215686, 0.7647058823529411, + 122, 0, 0.47843137254901963, 0.7607843137254902, + 123, 0, 0.4823529411764706, 0.7607843137254902, + 124, 0, 0.48627450980392156, 0.7568627450980392, + 125, 0, 0.49019607843137253, 0.7568627450980392, + 126, 0, 0.49411764705882355, 0.7529411764705882, + 127, 0, 0.4980392156862745, 0.7529411764705882, + 128, 0.00392156862745098, 0.5019607843137255, 0.7490196078431373, + 129, 0.00784313725490196, 0.5058823529411764, 0.7490196078431373, + 130, 0.011764705882352941, 0.5098039215686274, 0.7450980392156863, + 131, 0.01568627450980392, 0.5137254901960784, 0.7450980392156863, + 132, 0.0196078431372549, 0.5176470588235295, 0.7411764705882353, + 133, 0.023529411764705882, 0.5215686274509804, 0.7411764705882353, + 134, 0.027450980392156862, 0.5254901960784314, 0.7372549019607844, + 135, 0.03137254901960784, 0.5294117647058824, 0.7372549019607844, + 136, 0.03529411764705882, 0.5333333333333333, 0.7333333333333333, + 137, 0.0392156862745098, 0.5372549019607843, 0.7333333333333333, + 138, 0.043137254901960784, 0.5411764705882353, 0.7294117647058823, + 139, 0.047058823529411764, 0.5450980392156862, 0.7294117647058823, + 140, 0.050980392156862744, 0.5490196078431373, 0.7254901960784313, + 141, 0.054901960784313725, 0.5529411764705883, 0.7254901960784313, + 142, 0.058823529411764705, 0.5568627450980392, 0.7215686274509804, + 143, 0.06274509803921569, 0.5607843137254902, 0.7215686274509804, + 144, 0.06666666666666667, 0.5647058823529412, 0.7176470588235294, + 145, 0.07058823529411765, 0.5686274509803921, 0.7176470588235294, + 146, 0.07450980392156863, 0.5725490196078431, 0.7137254901960784, + 147, 0.0784313725490196, 0.5764705882352941, 0.7137254901960784, + 148, 0.08235294117647059, 0.5803921568627451, 0.7098039215686275, + 149, 0.08627450980392157, 0.5843137254901961, 0.7098039215686275, + 150, 0.09019607843137255, 0.5882352941176471, 0.7058823529411765, + 151, 0.09411764705882353, 0.592156862745098, 0.7058823529411765, + 152, 0.09803921568627451, 0.596078431372549, 0.7019607843137254, + 153, 0.10196078431372549, 0.6, 0.7019607843137254, + 154, 0.10588235294117647, 0.6039215686274509, 0.6980392156862745, + 155, 0.10980392156862745, 0.6078431372549019, 0.6980392156862745, + 156, 0.11372549019607843, 0.611764705882353, 0.6941176470588235, + 157, 0.11764705882352941, 0.615686274509804, 0.6941176470588235, + 158, 0.12156862745098039, 0.6196078431372549, 0.6901960784313725, + 159, 0.12549019607843137, 0.6235294117647059, 0.6901960784313725, + 160, 0.12941176470588237, 0.6274509803921569, 0.6862745098039216, + 161, 0.13333333333333333, 0.6313725490196078, 0.6862745098039216, + 162, 0.13725490196078433, 0.6352941176470588, 0.6823529411764706, + 163, 0.1411764705882353, 0.6392156862745098, 0.6823529411764706, + 164, 0.1450980392156863, 0.6431372549019608, 0.6784313725490196, + 165, 0.14901960784313725, 0.6470588235294118, 0.6784313725490196, + 166, 0.15294117647058825, 0.6509803921568628, 0.6745098039215687, + 167, 0.1568627450980392, 0.6549019607843137, 0.6745098039215687, + 168, 0.1607843137254902, 0.6588235294117647, 0.6705882352941176, + 169, 0.16470588235294117, 0.6627450980392157, 0.6705882352941176, + 170, 0.16862745098039217, 0.6666666666666666, 0.6666666666666666, + 171, 0.17254901960784313, 0.6705882352941176, 0.6666666666666666, + 172, 0.17647058823529413, 0.6745098039215687, 0.6627450980392157, + 173, 0.1803921568627451, 0.6784313725490196, 0.6627450980392157, + 174, 0.1843137254901961, 0.6823529411764706, 0.6588235294117647, + 175, 0.18823529411764706, 0.6862745098039216, 0.6588235294117647, + 176, 0.19215686274509805, 0.6901960784313725, 0.6549019607843137, + 177, 0.19607843137254902, 0.6941176470588235, 0.6549019607843137, + 178, 0.2, 0.6980392156862745, 0.6509803921568628, + 179, 0.20392156862745098, 0.7019607843137254, 0.6509803921568628, + 180, 0.20784313725490197, 0.7058823529411765, 0.6470588235294118, + 181, 0.21176470588235294, 0.7098039215686275, 0.6470588235294118, + 182, 0.21568627450980393, 0.7137254901960784, 0.6431372549019608, + 183, 0.2196078431372549, 0.7176470588235294, 0.6431372549019608, + 184, 0.2235294117647059, 0.7215686274509804, 0.6392156862745098, + 185, 0.22745098039215686, 0.7254901960784313, 0.6392156862745098, + 186, 0.23137254901960785, 0.7294117647058823, 0.6352941176470588, + 187, 0.23529411764705882, 0.7333333333333333, 0.6352941176470588, + 188, 0.23921568627450981, 0.7372549019607844, 0.6313725490196078, + 189, 0.24313725490196078, 0.7411764705882353, 0.6313725490196078, + 190, 0.24705882352941178, 0.7450980392156863, 0.6274509803921569, + 191, 0.25098039215686274, 0.7490196078431373, 0.6274509803921569, + 192, 0.25098039215686274, 0.7529411764705882, 0.6235294117647059, + 193, 0.2549019607843137, 0.7568627450980392, 0.6235294117647059, + 194, 0.25882352941176473, 0.7607843137254902, 0.6196078431372549, + 195, 0.2627450980392157, 0.7647058823529411, 0.6196078431372549, + 196, 0.26666666666666666, 0.7686274509803922, 0.615686274509804, + 197, 0.27058823529411763, 0.7725490196078432, 0.615686274509804, + 198, 0.27450980392156865, 0.7764705882352941, 0.611764705882353, + 199, 0.2784313725490196, 0.7803921568627451, 0.611764705882353, + 200, 0.2823529411764706, 0.7843137254901961, 0.6078431372549019, + 201, 0.28627450980392155, 0.788235294117647, 0.6078431372549019, + 202, 0.2901960784313726, 0.792156862745098, 0.6039215686274509, + 203, 0.29411764705882354, 0.796078431372549, 0.6039215686274509, + 204, 0.2980392156862745, 0.8, 0.6, + 205, 0.30196078431372547, 0.803921568627451, 0.6, + 206, 0.3058823529411765, 0.807843137254902, 0.596078431372549, + 207, 0.30980392156862746, 0.8117647058823529, 0.596078431372549, + 208, 0.3137254901960784, 0.8156862745098039, 0.592156862745098, + 209, 0.3176470588235294, 0.8196078431372549, 0.592156862745098, + 210, 0.3215686274509804, 0.8235294117647058, 0.5882352941176471, + 211, 0.3254901960784314, 0.8274509803921568, 0.5882352941176471, + 212, 0.32941176470588235, 0.8313725490196079, 0.5843137254901961, + 213, 0.3333333333333333, 0.8352941176470589, 0.5843137254901961, + 214, 0.33725490196078434, 0.8392156862745098, 0.5803921568627451, + 215, 0.3411764705882353, 0.8431372549019608, 0.5803921568627451, + 216, 0.34509803921568627, 0.8470588235294118, 0.5764705882352941, + 217, 0.34901960784313724, 0.8509803921568627, 0.5764705882352941, + 218, 0.35294117647058826, 0.8549019607843137, 0.5725490196078431, + 219, 0.3568627450980392, 0.8588235294117647, 0.5725490196078431, + 220, 0.3607843137254902, 0.8627450980392157, 0.5686274509803921, + 221, 0.36470588235294116, 0.8666666666666667, 0.5686274509803921, + 222, 0.3686274509803922, 0.8705882352941177, 0.5647058823529412, + 223, 0.37254901960784315, 0.8745098039215686, 0.5647058823529412, + 224, 0.3764705882352941, 0.8784313725490196, 0.5607843137254902, + 225, 0.3803921568627451, 0.8823529411764706, 0.5607843137254902, + 226, 0.3843137254901961, 0.8862745098039215, 0.5568627450980392, + 227, 0.38823529411764707, 0.8901960784313725, 0.5568627450980392, + 228, 0.39215686274509803, 0.8941176470588236, 0.5529411764705883, + 229, 0.396078431372549, 0.8980392156862745, 0.5529411764705883, + 230, 0.4, 0.9019607843137255, 0.5490196078431373, + 231, 0.403921568627451, 0.9058823529411765, 0.5490196078431373, + 232, 0.40784313725490196, 0.9098039215686274, 0.5450980392156862, + 233, 0.4117647058823529, 0.9137254901960784, 0.5450980392156862, + 234, 0.41568627450980394, 0.9176470588235294, 0.5411764705882353, + 235, 0.4196078431372549, 0.9215686274509803, 0.5411764705882353, + 236, 0.4235294117647059, 0.9254901960784314, 0.5372549019607843, + 237, 0.42745098039215684, 0.9294117647058824, 0.5372549019607843, + 238, 0.43137254901960786, 0.9333333333333333, 0.5333333333333333, + 239, 0.43529411764705883, 0.9372549019607843, 0.5333333333333333, + 240, 0.4392156862745098, 0.9411764705882353, 0.5294117647058824, + 241, 0.44313725490196076, 0.9450980392156862, 0.5294117647058824, + 242, 0.4470588235294118, 0.9490196078431372, 0.5254901960784314, + 243, 0.45098039215686275, 0.9529411764705882, 0.5254901960784314, + 244, 0.4549019607843137, 0.9568627450980393, 0.5215686274509804, + 245, 0.4588235294117647, 0.9607843137254902, 0.5215686274509804, + 246, 0.4627450980392157, 0.9647058823529412, 0.5176470588235295, + 247, 0.4666666666666667, 0.9686274509803922, 0.5176470588235295, + 248, 0.47058823529411764, 0.9725490196078431, 0.5137254901960784, + 249, 0.4745098039215686, 0.9764705882352941, 0.5137254901960784, + 250, 0.47843137254901963, 0.9803921568627451, 0.5098039215686274, + 251, 0.4823529411764706, 0.984313725490196, 0.5098039215686274, + 252, 0.48627450980392156, 0.9882352941176471, 0.5058823529411764, + 253, 0.49019607843137253, 0.9921568627450981, 0.5058823529411764, + 254, 0.49411764705882355, 0.996078431372549, 0.5019607843137255, + 255, 0.4980392156862745, 1, 0.5019607843137255 + ] + } +] diff --git a/src/vtk/LayerColorPresets.ts b/src/vtk/LayerColorPresets.ts new file mode 100644 index 000000000..1b767ddc0 --- /dev/null +++ b/src/vtk/LayerColorPresets.ts @@ -0,0 +1,25 @@ +import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps'; +import './ColorMaps'; + +export const DEFAULT_LAYER_PRESET = 'DICOM Hot Iron'; + +export const LAYER_PRESET_OPTIONS = [ + { title: 'Grayscale', value: 'Grayscale' }, + { title: 'Inverted grayscale', value: 'Inverted-Grayscale' }, + { title: 'DICOM Hot Iron', value: 'DICOM Hot Iron' }, + { title: 'DICOM PET', value: 'DICOM PET' }, + { title: 'DICOM Hot Metal Blue', value: 'DICOM Hot Metal Blue' }, + { title: 'DICOM Spring', value: 'DICOM Spring' }, + { title: 'DICOM Winter', value: 'DICOM Winter' }, + { title: 'Viridis', value: 'Viridis (matplotlib)' }, + { title: 'Cool to Warm', value: 'Cool to Warm' }, +]; + +export const resolveBaseColorPreset = (preset?: string) => + LAYER_PRESET_OPTIONS.find(({ value }) => value === preset)?.value ?? + 'Grayscale'; + +export const resolveLayerColorPreset = (preset?: string) => + preset && vtkColorMaps.getPresetByName(preset) + ? preset + : DEFAULT_LAYER_PRESET; diff --git a/vite.config.ts b/vite.config.ts index 76470fe91..d17b0d6a1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,8 +11,10 @@ import { viteStaticCopy } from 'vite-plugin-static-copy'; import { visualizer } from 'rollup-plugin-visualizer'; import { sentryVitePlugin } from '@sentry/vite-plugin'; import replace from '@rollup/plugin-replace'; +import VtkColorPresets from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps.json.js'; import { BASE_URL } from './tests/e2ePorts'; +import { LAYER_PRESET_OPTIONS } from './src/vtk/LayerColorPresets'; function resolveNodeModulePath(moduleName: string) { const require = createRequire(import.meta.url); @@ -31,6 +33,9 @@ function resolvePath(...args: string[]) { } const PLACEHOLDER_VERSION = '0.0.0'; +const sliceColorPresets = VtkColorPresets.filter(({ Name }: { Name: string }) => + LAYER_PRESET_OPTIONS.some(({ value }) => value === Name) +); function getPackageInfo() { const mainPkgPath = path.resolve(__dirname, 'package.json'); @@ -177,8 +182,8 @@ export default defineConfig({ load(id) { if (id.includes('@kitware/vtk.js')) { if (id.includes('ColorMaps.json.js')) { - // We don't use the built-in colormaps - return 'export default [];'; + // Bundle only the built-in palettes used by 2D overlays. + return `export default ${JSON.stringify(sliceColorPresets)};`; } // We don't use these classes From fdf391b1f6f4e2c2d073f2867b7bed6057aaeffb Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Wed, 21 Oct 2026 09:05:38 -0400 Subject: [PATCH 2/5] feat(rendering): add persistent base and overlay display controls Use shared controls for scalar palettes, display ranges, contrast, visibility, and overlay opacity. Apply base image settings in ordinary and oblique views, while preserving native RGB colors and RGBA alpha composition. Preserve display settings across view changes and saved sessions, including reset appearance and removed layers. Cover the controls, rendering, and saved state with behavior tests and document the display options. --- docs/loading_data.md | 2 +- docs/rendering.md | 29 +- docs/welcome_screen.md | 2 +- package-lock.json | 12 + package.json | 2 + src/components/BaseImageProperties.vue | 154 ++++ src/components/ControlsStripTools.vue | 50 +- src/components/LayerContrastPresets.vue | 76 ++ src/components/LayerList.vue | 41 +- src/components/LayerProperties.vue | 217 ++++-- src/components/ObliqueViewer.vue | 9 +- src/components/RenderingModule.vue | 86 ++- .../__tests__/LayerProperties.spec.ts | 397 +++++++++++ .../tools/windowing/WindowLevelControls.vue | 66 +- .../vtk/VtkBaseObliqueSliceRepresentation.vue | 56 +- .../vtk/VtkBaseSliceRepresentation.vue | 33 +- .../vtk/VtkLayerSliceRepresentation.vue | 146 +++- .../vtk/VtkSliceViewWindowManipulator.vue | 4 +- src/composables/useBaseSliceColoring.ts | 30 + src/composables/useColoringEffect.ts | 9 +- src/composables/useWindowingConfig.ts | 18 +- src/config.ts | 7 +- src/core/dicomTags.ts | 1 + .../layerDisplaySaveSnapshot.spec.ts | 215 ++++++ src/io/state-file/schema.ts | 20 +- src/io/state-file/serialize.ts | 1 + src/store/datasets-layers.ts | 66 +- src/store/view-configs.ts | 27 +- src/store/view-configs/common.ts | 5 + src/store/view-configs/layers.ts | 306 +++++--- src/store/view-configs/types.ts | 15 +- src/store/view-configs/windowing.ts | 28 +- src/store/views.ts | 4 +- src/utils/__tests__/dicomWindowRange.spec.ts | 97 +++ src/utils/dicomWindowRange.ts | 57 ++ src/utils/layers.ts | 2 + src/utils/nativeColorWindow.ts | 17 + .../layers_max_opacity_2d_view-chrome-1.png | Bin 217013 -> 0 bytes tests/specs/base-image-availability.e2e.ts | 94 +++ tests/specs/base-image-display.e2e.ts | 585 ++++++++++++++++ tests/specs/layer-display-controls.e2e.ts | 661 ++++++++++++++++++ tests/specs/layerDisplayTestUtils.ts | 33 + tests/specs/layers-and-rendering.e2e.ts | 149 ++-- tests/specs/legacy-window-appearance.e2e.ts | 134 ++++ .../specs/multicomponent-layer-display.e2e.ts | 426 +++++++++++ tests/specs/overlay-auto-contrast.e2e.ts | 311 ++++++++ .../specs/overlay-segmentation-opacity.e2e.ts | 148 ++++ tests/specs/palette-window.e2e.ts | 569 +++++++++++++++ tests/specs/range-number-steppers.e2e.ts | 184 +++++ tests/specs/transparent-background.e2e.ts | 260 +++++++ tests/specs/utils.ts | 125 ++++ 51 files changed, 5547 insertions(+), 439 deletions(-) create mode 100644 src/components/BaseImageProperties.vue create mode 100644 src/components/LayerContrastPresets.vue create mode 100644 src/components/__tests__/LayerProperties.spec.ts create mode 100644 src/composables/useBaseSliceColoring.ts create mode 100644 src/io/state-file/__tests__/layerDisplaySaveSnapshot.spec.ts create mode 100644 src/utils/__tests__/dicomWindowRange.spec.ts create mode 100644 src/utils/dicomWindowRange.ts create mode 100644 src/utils/layers.ts create mode 100644 src/utils/nativeColorWindow.ts delete mode 100644 tests/baseline/layers_max_opacity_2d_view-chrome-1.png create mode 100644 tests/specs/base-image-availability.e2e.ts create mode 100644 tests/specs/base-image-display.e2e.ts create mode 100644 tests/specs/layer-display-controls.e2e.ts create mode 100644 tests/specs/layerDisplayTestUtils.ts create mode 100644 tests/specs/legacy-window-appearance.e2e.ts create mode 100644 tests/specs/multicomponent-layer-display.e2e.ts create mode 100644 tests/specs/overlay-auto-contrast.e2e.ts create mode 100644 tests/specs/overlay-segmentation-opacity.e2e.ts create mode 100644 tests/specs/palette-window.e2e.ts create mode 100644 tests/specs/range-number-steppers.e2e.ts create mode 100644 tests/specs/transparent-background.e2e.ts diff --git a/docs/loading_data.md b/docs/loading_data.md index dce2ce0c8..88f422af2 100644 --- a/docs/loading_data.md +++ b/docs/loading_data.md @@ -64,7 +64,7 @@ To layer images: 1. Load the base image. 1. On the Data tab, click the Add Layer button on the upper layer dataset. -1. Under the Rendering tab, an opacity slider changes the transparency of the upper layer. +1. Under the Rendering tab, open Layers to adjust the overlay color map, display range, opacity, and transparent background. ![Add Layer](./assets/add-layer.jpg) diff --git a/docs/rendering.md b/docs/rendering.md index 11edadb5a..86007f241 100644 --- a/docs/rendering.md +++ b/docs/rendering.md @@ -1,17 +1,36 @@ -# Cinematic Volume Rendering +# Rendering -VolView reads the DICOM tags of your data to determine appropriate preset parameter values for cinematic volume rendering for your data, but often you will want to tweak those presets to emphasize specific details. Rendering controls apply to the currently selected/active view. We recommend the following sequence of tweaks to improve your visualizations. +## Image layers in 2D views + +Open **Layers** in the Rendering tab to change scalar image color maps, +display ranges, visibility, and overlay opacity. The base image has its own +color map and range controls in ordinary 2D and oblique views. Overlay layers +render in ordinary 2D views. + +RGB and RGBA images use their original colors in slice views. Scalar color maps +and window/level do not recolor them, including when opening saved sessions. +RGBA overlays multiply their embedded alpha by the layer opacity. Integer color +channels use their type's zero-based range; floating-point channels use 0 to 1. + +The color bar always spans the image's full intensity range, even when the +selected display range extends beyond it. -1. Cinematic volume rendering involves two "transfer functions": one maps the recorded intensity values (e.g., CT hounsfield units) to opacity and the other maps recorded intensity values to color. Both can be adjusted using the controls at the top of the Rendering tab. The graph at the top shows a histogram of the recorded intensity values in light gray and the current opacity transfer function as an overlaid curve in black. In that graph, beneath the black curve, is a depiction of the color transfer function. ![Cinematic](./assets/16-volview-rendering.jpg) +**Transparent background** makes overlay values at or below Min transparent, +then fades linearly to the selected opacity over the first 10% of the display +range above Min. +## Cinematic rendering controls + +VolView reads the DICOM tags of your data to determine appropriate preset parameter values for cinematic volume rendering for your data, but often you will want to tweak those presets to emphasize specific details. Rendering controls apply to the currently selected/active view. We recommend the following sequence of tweaks to improve your visualizations. + +1. Cinematic volume rendering involves two "transfer functions": one maps the recorded intensity values (e.g., CT hounsfield units) to opacity and the other maps recorded intensity values to color. Both can be adjusted by expanding **3D Rendering** in the Rendering tab. The graph at the top shows a histogram of the recorded intensity values in light gray and the current opacity transfer function as an overlaid curve in black. In that graph, beneath the black curve, is a depiction of the color transfer function. ![Cinematic](./assets/16-volview-rendering.jpg) 1. Begin by adjusting the opacity transfer function by a press-and-drag right/left on the top graph. ![Opacity](./assets/17-volview-opacity-notes.jpg) 2. Then adjust the color transfer function by moving the blue dots on the colorbar beneath the graph. ![Color](./assets/17-volview-colormap-notes.jpg) -2. You may decide that the colormaps and/or transfer function aren't suitable for your data. Click on the "Presets" bar to expand and show the available presets that offer alternative opacity and color transfer functions. ![Presets](./assets/18-volview-presets.jpg) +2. You may decide that the colormaps and/or transfer function aren't suitable for your data. Expand **Color Presets** within **3D Rendering** to show the available presets that offer alternative opacity and color transfer functions. ![Presets](./assets/18-volview-presets.jpg) [**_Watch the video!_**](https://youtu.be/eyrGd-meg6I) 3. Next, the lighting can be controlled. - 1. Ambient lighting is the general brightness of the scene. 2. Diffuse lighting controls how the light reflects off the data. It is influenced by the position of the light relative to the orientation of the data's local surface. 3. Light-follows-camera can be enabled / disabled to create shadows that highlights details within the data. When enabled, the light will be positioned in-line with the camera. When disabled, the last position of the light stays fixed relative to the data, even if the camera is moved. With light-follows-camera enabled, here is a view when the light is following the camera: ![Default Lighting](./assets/20-volview-lightfollowcamera1.jpg). By moving the camera (and thereby the light) to the front of the data and then disabling light-follows-camera, the view should look light this: ![Light Setup](./assets/20-volview-lightfollowcamera2.jpg). Then moving back to the side view will show shadows that emphasize depth and details: ![Shadows](./assets/20-volview-lightfollowcamera3.jpg). diff --git a/docs/welcome_screen.md b/docs/welcome_screen.md index 92df151c9..8081863ec 100644 --- a/docs/welcome_screen.md +++ b/docs/welcome_screen.md @@ -16,7 +16,7 @@ Lists the ruler and other measures that have been made on the currently loaded d ### Rendering -Controls for the 3D cinematic volume rendering. +Controls for 3D cinematic rendering and 2D image layers. See [Rendering](./rendering.md) for details. ## Load / Save State diff --git a/package-lock.json b/package-lock.json index 4b9e40a87..c7190f46a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@types/express": "^5.0.5", "@types/file-saver": "^2.0.7", "@types/mocha": "^10.0.10", + "@types/pngjs": "^6.0.5", "@velipso/polybool": "^2.0.11", "@vitejs/plugin-vue": "^6.0.2", "@vue/test-utils": "^2.4.6", @@ -63,6 +64,7 @@ "nanoid": "^5.1.6", "patch-package": "^8.0.1", "pinia": "^3.0.4", + "pngjs": "^7.0.0", "prettier": "3.7.2", "rollup-plugin-visualizer": "^6.0.5", "sass": "^1.94.2", @@ -5345,6 +5347,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/pngjs": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.5.tgz", + "integrity": "sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", diff --git a/package.json b/package.json index 825153889..2ad40f8b3 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@types/express": "^5.0.5", "@types/file-saver": "^2.0.7", "@types/mocha": "^10.0.10", + "@types/pngjs": "^6.0.5", "@velipso/polybool": "^2.0.11", "@vitejs/plugin-vue": "^6.0.2", "@vue/test-utils": "^2.4.6", @@ -89,6 +90,7 @@ "nanoid": "^5.1.6", "patch-package": "^8.0.1", "pinia": "^3.0.4", + "pngjs": "^7.0.0", "prettier": "3.7.2", "rollup-plugin-visualizer": "^6.0.5", "sass": "^1.94.2", diff --git a/src/components/BaseImageProperties.vue b/src/components/BaseImageProperties.vue new file mode 100644 index 000000000..dc4471295 --- /dev/null +++ b/src/components/BaseImageProperties.vue @@ -0,0 +1,154 @@ + + + diff --git a/src/components/ControlsStripTools.vue b/src/components/ControlsStripTools.vue index 79caf4fb5..7513ff647 100644 --- a/src/components/ControlsStripTools.vue +++ b/src/components/ControlsStripTools.vue @@ -9,15 +9,29 @@ v-slot:default="{ active, toggle }" :value="Tools.WindowLevel" > - - - + + !currentImageID.value); const currentTool = computed(() => toolStore.currentTool); + const windowingUnavailableReason = computed(() => { + if (!currentImageID.value) return 'Load an image to adjust window/level.'; + const scalars = currentImageData.value?.getPointData().getScalars(); + if (!scalars) return 'The image must finish loading first.'; + if (isCineImage(currentImageID.value)) + return 'Cine images already contain display-encoded pixels.'; + const components = scalars.getNumberOfComponents(); + if (components === 3 || components === 4) + return 'RGB and RGBA images use their original colors.'; + return ''; + }); + const activeViewRef = toRef(viewStore, 'activeView'); const activeEffective = useEffectiveView( computed(() => activeViewRef.value ?? '') ); + // The rendered viewer is decided by effective kind, not stored slot type: // a cine clip dropped into an Oblique slot still renders as cine, so the // toolbar should treat it as cine, not Oblique. const isObliqueLayout = computed( () => activeEffective.value?.kind === 'oblique' ); + const isCineActive = computed(() => activeEffective.value?.kind === 'cine'); const isDisallowedOnCine = (tool: Tools) => isCineActive.value && !isToolAllowedFor(tool, activeEffective.value); + const paintUnavailableReason = computed( () => toolStore.paintUnavailableReason ); const enableTempCrosshairs = useActionHeld('temporaryCrosshairs'); + watch(enableTempCrosshairs, (enable) => { if (enable) toolStore.activateTemporaryCrosshairs(); else toolStore.deactivateTemporaryCrosshairs(); @@ -205,6 +236,7 @@ export default defineComponent({ currentTool, setCurrentTool: toolStore.setCurrentTool, noCurrentImage, + windowingUnavailableReason, isObliqueLayout, isDisallowedOnCine, paintUnavailableReason, diff --git a/src/components/LayerContrastPresets.vue b/src/components/LayerContrastPresets.vue new file mode 100644 index 000000000..52e221e74 --- /dev/null +++ b/src/components/LayerContrastPresets.vue @@ -0,0 +1,76 @@ + + + diff --git a/src/components/LayerList.vue b/src/components/LayerList.vue index b15861756..d1d9cd2b3 100644 --- a/src/components/LayerList.vue +++ b/src/components/LayerList.vue @@ -1,32 +1,25 @@ - diff --git a/src/components/LayerProperties.vue b/src/components/LayerProperties.vue index 37d0b1a9f..5ae4cbbf6 100644 --- a/src/components/LayerProperties.vue +++ b/src/components/LayerProperties.vue @@ -1,88 +1,163 @@ - diff --git a/src/components/ObliqueViewer.vue b/src/components/ObliqueViewer.vue index 8ec44e2a7..44f972c8f 100644 --- a/src/components/ObliqueViewer.vue +++ b/src/components/ObliqueViewer.vue @@ -56,6 +56,8 @@ diff --git a/src/components/vtk/VtkSliceViewWindowManipulator.vue b/src/components/vtk/VtkSliceViewWindowManipulator.vue index d47c069db..41b1fa254 100644 --- a/src/components/vtk/VtkSliceViewWindowManipulator.vue +++ b/src/components/vtk/VtkSliceViewWindowManipulator.vue @@ -48,14 +48,16 @@ const { instance: rangeManipulator } = useVtkInteractionManipulator( ); const wlConfig = useWindowingConfig(viewId, imageId); + useResetViewsEvents().onClick(() => { - useWindowingStore().resetConfig(imageId.value, viewId.value); + useWindowingStore().resetConfig(wlConfig.viewID.value, imageId.value); }); const computeStep = (range: Vector2) => { const diff = range[1] - range[0] || 1; return Math.min(diff, 1) / 256; }; + const wlStep = computed(() => computeStep(wlConfig.range.value)); const horiz = useMouseRangeManipulatorListener( diff --git a/src/composables/useBaseSliceColoring.ts b/src/composables/useBaseSliceColoring.ts new file mode 100644 index 000000000..8ccecdc8e --- /dev/null +++ b/src/composables/useBaseSliceColoring.ts @@ -0,0 +1,30 @@ +import { MaybeRef, onScopeDispose, unref, watchEffect } from 'vue'; +import vtkImageProperty from '@kitware/vtk.js/Rendering/Core/ImageProperty'; +import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; +import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps'; +import { resolveBaseColorPreset } from '@/src/vtk/LayerColorPresets'; + +export function useBaseSliceColoring( + property: vtkImageProperty, + preset: MaybeRef, + enabled: MaybeRef, + requestRender: () => void +) { + const colors = vtkColorTransferFunction.newInstance(); + onScopeDispose(() => colors.delete()); + + watchEffect(() => { + const name = resolveBaseColorPreset(unref(preset)); + if (!unref(enabled) || name === 'Grayscale') { + property.setRGBTransferFunction(0, null); + } else { + colors.applyColorMap(vtkColorMaps.getPresetByName(name)!); + colors.setMappingRange(0, 1); + colors.updateRange(); + property.setRGBTransferFunction(0, colors); + } + // The image property's window/level maps scalar values onto the palette. + property.setUseLookupTableScalarRange(false); + requestRender(); + }); +} diff --git a/src/composables/useColoringEffect.ts b/src/composables/useColoringEffect.ts index 17fca1034..9d7816fce 100644 --- a/src/composables/useColoringEffect.ts +++ b/src/composables/useColoringEffect.ts @@ -26,7 +26,7 @@ type ApplyColoringParams = { ofun: vtkPiecewiseFunction; }; -export function applyColoring({ +function applyColoring({ props: { colorFunction, opacityFunction }, cfun, ofun, @@ -42,6 +42,13 @@ export function applyColoring({ cfun.setMappingRange(...colorFunction.mappingRange); } + applyOpacityFunction(opacityFunction, ofun); +} + +export function applyOpacityFunction( + opacityFunction: OpacityFunction, + ofun: vtkPiecewiseFunction +) { const { mappingRange } = opacityFunction; if (!isZeroWidthRange(opacityFunction.mappingRange)) { ofun.setRange(...opacityFunction.mappingRange); diff --git a/src/composables/useWindowingConfig.ts b/src/composables/useWindowingConfig.ts index 8de67ed56..1e56f5b02 100644 --- a/src/composables/useWindowingConfig.ts +++ b/src/composables/useWindowingConfig.ts @@ -1,19 +1,25 @@ import { useWindowingStore } from '@/src/store/view-configs/windowing'; import { Maybe } from '@/src/types'; import type { Vector2 } from '@kitware/vtk.js/types'; -import { MaybeRef, unref, computed } from 'vue'; +import { MaybeRef, unref, computed, inject, InjectionKey } from 'vue'; import { useImageStatsStore } from '@/src/store/image-stats'; +export const WindowingViewContext: InjectionKey> = Symbol( + 'WindowingViewContext' +); + export function useWindowingConfig( viewID: MaybeRef, imageID: MaybeRef> ) { + const windowingViewID = inject(WindowingViewContext, viewID); const store = useWindowingStore(); const imageStatsStore = useImageStatsStore(); + const config = computed(() => { const imageIdVal = unref(imageID); if (!imageIdVal) return undefined; - const viewIdVal = unref(viewID); + const viewIdVal = unref(windowingViewID); if (!viewIdVal) return undefined; return store.getConfig(viewIdVal, imageIdVal); }); @@ -28,7 +34,12 @@ export function useWindowingConfig( set: (val) => { const imageIdVal = unref(imageID); if (!imageIdVal || val == null) return; - store.updateConfig(unref(viewID), imageIdVal, { [prop]: val }, true); + store.updateConfig( + unref(windowingViewID), + imageIdVal, + { [prop]: val }, + true + ); }, }); }; @@ -43,6 +54,7 @@ export function useWindowingConfig( }); return { + viewID: computed(() => unref(windowingViewID)), config, width: generateComputed('width'), level: generateComputed('level'), diff --git a/src/config.ts b/src/config.ts index 8e1b9d12a..ac2114942 100644 --- a/src/config.ts +++ b/src/config.ts @@ -175,13 +175,8 @@ export const DEFAULT_PRESET_BY_MODALITY: Record = { MR: 'CT-Coronary-Arteries-2', US: 'US-Fetal', }; -export const DEFAULT_PRESET = 'CT-AAA'; -export const LAYER_PRESET_BY_MODALITY: Record = { - ...DEFAULT_PRESET_BY_MODALITY, - PT: '2hot-opaque', -}; -export const LAYER_PRESET_DEFAULT = '2hot-opaque'; +export const DEFAULT_PRESET = 'CT-AAA'; // Keyboard shortcuts/hotkeys. Can add modifiers: 'Shift+Ctrl+a' export const ACTION_TO_KEY = { diff --git a/src/core/dicomTags.ts b/src/core/dicomTags.ts index fe2e13cc7..d042b06ea 100644 --- a/src/core/dicomTags.ts +++ b/src/core/dicomTags.ts @@ -21,6 +21,7 @@ const tags: Tag[] = [ { name: 'SeriesDescription', tag: '0008|103e' }, { name: 'WindowLevel', tag: '0028|1050' }, { name: 'WindowWidth', tag: '0028|1051' }, + { name: 'VOILUTFunction', tag: '0028|1056' }, { name: 'Rows', tag: '0028|0010' }, { name: 'Columns', tag: '0028|0011' }, { name: 'BitsAllocated', tag: '0028|0100' }, diff --git a/src/io/state-file/__tests__/layerDisplaySaveSnapshot.spec.ts b/src/io/state-file/__tests__/layerDisplaySaveSnapshot.spec.ts new file mode 100644 index 000000000..e0e20bedf --- /dev/null +++ b/src/io/state-file/__tests__/layerDisplaySaveSnapshot.spec.ts @@ -0,0 +1,215 @@ +import { expect, it } from 'vitest'; +import { nextTick } from 'vue'; +import JSZip from 'jszip'; +import { createStateFileRestorer } from '@/src/io/import/processors/restoreStateFile'; +import { MANIFEST, serialize } from '@/src/io/state-file/serialize'; +import { ManifestSchema, type Manifest } from '@/src/io/state-file/schema'; +import { + activateAppPinia, + seatImage, +} from '@/src/segmentation/__tests__/segmentMaskFixtures'; +import { seatDataSource } from '@/src/store/__tests__/datasetFixtures'; +import { useImageStatsStore } from '@/src/store/image-stats'; +import { useLayersStore } from '@/src/store/datasets-layers'; +import { useViewStore } from '@/src/store/views'; +import { useLayerColoringStore } from '@/src/store/view-configs/layers'; +import { useWindowingStore } from '@/src/store/view-configs/windowing'; +import { useViewCameraStore } from '@/src/store/view-configs/camera'; +import { useViewSliceStore } from '@/src/store/view-configs/slicing'; +import { useSyncWindowing } from '@/src/composables/useSyncWindowing'; +import { getLayerId } from '@/src/utils/layers'; + +function seatStatistics(id: string) { + useImageStatsStore().stats[id] = { + scalarMin: 0, + scalarMax: 100, + autoRangeValues: {}, + }; +} + +async function loadImages(base: string, overlay: string) { + activateAppPinia(); + for (const id of [base, overlay]) { + seatStatistics(id); + await seatImage(id, { + dimensions: [2, 2, 2], + values: Float32Array.from([0, 20, 30, 40, 50, 60, 80, 100]), + }); + seatDataSource(id, { type: 'uri', uri: `/${id}.nrrd`, name: `${id}.nrrd` }); + } + seatStatistics(getLayerId(base, overlay)); +} + +const views2D = () => + useViewStore() + .getAllViews() + .filter((view) => view.type === '2D'); + +async function prepareScene() { + await loadImages('base', 'overlay'); + const layerID = (await useLayersStore()._addLayer('base', 'overlay'))!; + useViewStore().setDataForAllViews('base'); + const coloring = useLayerColoringStore(); + views2D().forEach((view) => coloring.getConfig(view.id, layerID)); + coloring.updateDisplayConfig(layerID, { opacity: 0.8 }); + const retainedView = views2D()[1]; + useWindowingStore().updateConfig(retainedView.id, 'base', { + width: 40, + level: 25, + useAuto: false, + }); + return { layerID, retainedView }; +} + +async function exportManifest() { + const archive = await JSZip.loadAsync(await serialize()); + const json = await archive.file(MANIFEST)!.async('string'); + return ManifestSchema.parse(JSON.parse(json)); +} + +async function restoreManifest(manifest: Manifest) { + await loadImages('restoredBase', 'restoredOverlay'); + const views = useViewStore(); + views.deserializeLayout(manifest); + await createStateFileRestorer()(manifest, [], { + base: 'restoredBase', + overlay: 'restoredOverlay', + }); + const layerID = (await useLayersStore()._addLayer( + 'restoredBase', + 'restoredOverlay' + ))!; + views.switchToNamedLayout('Four Up'); + views.visibleViews + .filter((view) => view.type === '2D') + .forEach((view) => useLayerColoringStore().getConfig(view.id, layerID)); + return layerID; +} + +it.each([false, true])( + 'drops removed overlay settings from repeated saves (re-added: %s)', + async (readd) => { + const { layerID, retainedView } = await prepareScene(); + await exportManifest(); + const views = useViewStore(); + views.switchToNamedLayout('Axial Only'); + const layers = useLayersStore(); + layers.deleteLayer('base', 'overlay'); + await nextTick(); + if (readd) { + seatStatistics(layerID); + await layers._addLayer('base', 'overlay'); + useLayerColoringStore().getConfig(views.visibleViews[0].id, layerID); + } + + const saved = await exportManifest(); + const savedViewConfig = saved.viewByID![retainedView.id].config!; + expect(savedViewConfig[layerID]?.layers).toBeUndefined(); + expect(savedViewConfig.base.window).toMatchObject({ width: 40, level: 25 }); + if (!readd) { + expect(saved.layerDisplay?.[layerID]).toBeUndefined(); + expect(saved.parentToLayers![0].sourceSelectionKeys).toEqual([]); + return; + } + + const restoredLayerID = await restoreManifest(saved); + const reexported = await exportManifest(); + expect(reexported.layoutSlots).toContain(retainedView.id); + expect( + Object.values(reexported.viewByID!) + .filter((view) => view.type === '2D') + .map( + (view) => view.config![restoredLayerID].layers!.blendConfig.opacity + ) + ).toEqual([0.3, 0.3, 0.3]); + expect(reexported.layerDisplay![restoredLayerID].opacity).toBe(0.3); + expect( + reexported.viewByID![retainedView.id].config!.restoredBase.window + ).toMatchObject({ width: 40, level: 25 }); + } +); + +it('preserves current per-view appearance and other view settings across repeated saves', async () => { + const { layerID, retainedView } = await prepareScene(); + const coloring = useLayerColoringStore(); + const original = coloring.getConfig(retainedView.id, layerID); + coloring.deserialize(retainedView.id, { + [layerID]: { + layers: { + ...original, + blendConfig: { opacity: 0.65, visibility: false }, + }, + }, + }); + await exportManifest(); + useViewStore().switchToNamedLayout('Axial Only'); + const saved = await exportManifest(); + const restoredLayerID = await restoreManifest(saved); + const reexported = await exportManifest(); + const restoredViewConfig = reexported.viewByID![retainedView.id].config!; + + expect(restoredViewConfig[restoredLayerID].layers!.blendConfig).toEqual({ + opacity: 0.65, + visibility: false, + }); + expect(reexported.layerDisplay![restoredLayerID].opacity).toBe(0.8); + expect(restoredViewConfig.restoredBase.window).toMatchObject({ + width: 40, + level: 25, + }); +}); + +it('saves reset base appearance without losing retained slice and camera settings', async () => { + await loadImages('base', 'overlay'); + useSyncWindowing(); + const views = useViewStore(); + views.setDataForAllViews('base'); + views.switchToNamedLayout('Oblique'); + const obliqueView = views.visibleViews[0]; + const retainedView = views2D()[0]; + useViewSliceStore().updateConfig(retainedView.id, 'base', { slice: 0 }); + useViewCameraStore().updateConfig(retainedView.id, 'base', { + parallelScale: 2, + }); + const windowing = useWindowingStore(); + windowing.updateConfig(obliqueView.id, 'base', { + preset: 'DICOM PET', + visibility: false, + width: 40, + level: 25, + }); + const beforeReset = await exportManifest(); + expect( + beforeReset.viewByID![obliqueView.id].config!.base.window + ).toMatchObject({ + preset: 'DICOM PET', + visibility: false, + }); + + windowing.resetConfig(obliqueView.id, 'base'); + const saved = await exportManifest(); + expect(saved.viewByID![obliqueView.id].config?.base?.window).toBeUndefined(); + + await loadImages('restoredBase', 'restoredOverlay'); + useSyncWindowing(); + useViewStore().deserializeLayout(saved); + await createStateFileRestorer()(saved, [], { + base: 'restoredBase', + overlay: 'restoredOverlay', + }); + const restored = await exportManifest(); + expect( + restored.viewByID![obliqueView.id].config!.restoredBase.window + ).toMatchObject({ + preset: 'Grayscale', + visibility: true, + width: 100, + level: 50, + }); + expect( + restored.viewByID![retainedView.id].config!.restoredBase + ).toMatchObject({ + slice: { slice: 0 }, + camera: { parallelScale: 2 }, + }); +}); diff --git a/src/io/state-file/schema.ts b/src/io/state-file/schema.ts index 15796d7eb..aa17f2a8b 100644 --- a/src/io/state-file/schema.ts +++ b/src/io/state-file/schema.ts @@ -1,3 +1,4 @@ +import { DEFAULT_LAYER_PRESET } from '@/src/vtk/LayerColorPresets'; import JSZip from 'jszip'; import { z } from 'zod'; import type { Vector3 } from '@kitware/vtk.js/types'; @@ -20,6 +21,7 @@ import type { SliceConfig, WindowLevelConfig, LayersConfig, + LayerDisplayConfig, VolumeColorConfig, CinePlaybackViewConfig, } from '@/src/store/view-configs/types'; @@ -136,8 +138,11 @@ const Vector3 = z.tuple([ ]) satisfies z.ZodType; type AutoRangeKeys = keyof typeof WLAutoRanges; + const WindowLevelConfig = z .object({ + preset: z.string().default('Grayscale'), + visibility: z.boolean().default(true), width: z.number().optional(), level: z.number().optional(), auto: z.string() as z.ZodType, @@ -253,13 +258,23 @@ const BlendConfig = z.object({ visibility: z.boolean(), }) satisfies z.ZodType; +const LayerColorTransferFunction = ColorTransferFunction.extend({ + preset: z.string().default(DEFAULT_LAYER_PRESET), +}); + const LayersConfig = z.object({ - colorBy: ColorBy, - transferFunction: ColorTransferFunction, + transferFunction: LayerColorTransferFunction, opacityFunction: OpacityFunction, blendConfig: BlendConfig, + transparentBackground: z.boolean().default(true), }) satisfies z.ZodType; +const LayerDisplayConfig = LayerColorTransferFunction.extend({ + opacity: z.number(), + visibility: z.boolean().default(true), + transparentBackground: z.boolean().default(true), +}) satisfies z.ZodType; + const CinePlaybackViewConfig = z.object({ frame: z.number(), }) satisfies z.ZodType; @@ -510,6 +525,7 @@ export const ManifestSchema = z.object({ layout: Layout.optional(), layoutSlots: z.array(z.string()).optional(), parentToLayers: ParentToLayers.optional(), + layerDisplay: z.record(z.string(), LayerDisplayConfig).optional(), }); export type Manifest = z.infer; diff --git a/src/io/state-file/serialize.ts b/src/io/state-file/serialize.ts index 89b998bcb..9a835f2cb 100644 --- a/src/io/state-file/serialize.ts +++ b/src/io/state-file/serialize.ts @@ -247,6 +247,7 @@ export function normalizeManifest(manifest: Manifest, zip: JSZip) { // is assembled from the parsed pieces, so nothing is validated (or // deep-copied) twice. const optionalRoots = [ + 'layerDisplay', 'tools', 'segments', 'selectedSegment', diff --git a/src/store/datasets-layers.ts b/src/store/datasets-layers.ts index f87ba8405..c5dd53f5f 100644 --- a/src/store/datasets-layers.ts +++ b/src/store/datasets-layers.ts @@ -6,6 +6,8 @@ import { Maybe } from '@/src/types'; import { ensureSameSpace } from '@/src/io/resample/resample'; import { useImageCacheStore } from '@/src/store/image-cache'; import { NO_NAME } from '@/src/constants'; +import { getLayerId } from '@/src/utils/layers'; +import { useLayerColoringStore } from './view-configs/layers'; import { useErrorMessage } from '../composables/useErrorMessage'; import { Manifest, StateFile } from '../io/state-file/schema'; import { untilLoaded } from '../composables/untilLoaded'; @@ -19,18 +21,10 @@ export const useLayersStore = defineStore('layer', () => { const imageCacheStore = useImageCacheStore(); const parentToLayers = reactive>({}); + const pendingLayers = new Map>(); - async function _addLayer(parent: DataSelection, source: DataSelection) { - if (!parent) { - throw new Error('Tried to addLayer without parent data selection'); - } - - const id = `${parent}::${source}` as string; - parentToLayers[parent] = [ - ...(parentToLayers[parent] ?? []), - { selection: source, id } as Layer, - ]; - + async function buildLayerImage(parent: DataSelection, layer: Layer) { + const { selection: source, id } = layer; // ensureSameSpace need final image array to resample, so wait for all chunks await untilLoaded(source); @@ -51,7 +45,6 @@ export const useLayersStore = defineStore('layer', () => { sourceImage.getBounds() ) ) { - deleteLayer(parent, source); throw new Error( 'Image bounds do not intersect, so no overlap in physical space' ); @@ -59,28 +52,41 @@ export const useLayersStore = defineStore('layer', () => { const image = await ensureSameSpace(parentImage, sourceImage); // Deleted while resampling: caching the image now would leave it unowned. - if (!parentToLayers[parent]?.some((layer) => layer.id === id)) - return undefined; + if (!parentToLayers[parent]?.includes(layer)) return undefined; const name = imageCacheStore.getImageMetadata(source)?.name ?? NO_NAME; imageCacheStore.addVTKImageData(image, name, { id }); return id; } + async function _addLayer(parent: DataSelection, source: DataSelection) { + if (!parent) { + throw new Error('Tried to addLayer without parent data selection'); + } + + const id = getLayerId(parent, source); + const pending = pendingLayers.get(id); + if (pending) return pending; + if (parentToLayers[parent]?.some((layer) => layer.id === id)) return id; + + const layer = reactive({ selection: source, id }); + parentToLayers[parent] = [...(parentToLayers[parent] ?? []), layer]; + const task = buildLayerImage(parent, layer); + pendingLayers.set(id, task); + try { + return await task; + } catch (error) { + if (parentToLayers[parent]?.includes(layer)) deleteLayer(parent, source); + throw error; + } finally { + if (pendingLayers.get(id) === task) pendingLayers.delete(id); + } + } + async function addLayer(parent: DataSelection, source: DataSelection) { - return useErrorMessage('Failed to build layer', async () => { - try { - return await _addLayer(parent, source); - } catch (error) { - // remove failed layer from parent's layer list - const layers = parentToLayers[parent]; - if (layers) - parentToLayers[parent] = layers.filter( - ({ selection }) => selection !== source - ); - throw error; - } - }); + return useErrorMessage('Failed to build layer', () => + _addLayer(parent, source) + ); } function deleteLayer(parent: DataSelection, source: DataSelection) { @@ -93,9 +99,13 @@ export const useLayersStore = defineStore('layer', () => { const layerToDelete = layers.find(({ selection }) => selection === source); if (!layerToDelete) return; - parentToLayers[parent] = layers.filter((layer) => layer !== layerToDelete); + parentToLayers[parent] = layers.filter( + (layer) => layer.id !== layerToDelete.id + ); + pendingLayers.delete(layerToDelete.id); imageCacheStore.removeImage(layerToDelete.id); + useLayerColoringStore().removeData(layerToDelete.id); } function getLayers(key: Maybe) { diff --git a/src/store/view-configs.ts b/src/store/view-configs.ts index 7dc0e9ac0..b07601838 100644 --- a/src/store/view-configs.ts +++ b/src/store/view-configs.ts @@ -1,4 +1,5 @@ import { defineStore } from 'pinia'; +import { getLayerId } from '@/src/utils/layers'; import useViewSliceStore from './view-configs/slicing'; import { useWindowingStore } from './view-configs/windowing'; @@ -74,11 +75,31 @@ export const useViewConfigStore = defineStore('viewConfig', () => { manifest: StateFile['manifest'], dataIDMap: Record ) => { - if (!manifest.viewByID) return; + // Layer images are rebuilt after view settings are restored. + const configIDMap = { ...dataIDMap }; + manifest.parentToLayers?.forEach( + ({ selectionKey, sourceSelectionKeys }) => { + const parent = dataIDMap[selectionKey]; + if (!parent) return; + sourceSelectionKeys.forEach((sourceKey) => { + const source = dataIDMap[sourceKey]; + if (!source) return; + configIDMap[getLayerId(selectionKey, sourceKey)] = getLayerId( + parent, + source + ); + }); + } + ); + + layerColoringStore.deserializeDisplayConfigs( + manifest.layerDisplay ?? {}, + configIDMap + ); - Object.entries(manifest.viewByID).forEach(([viewID, view]) => { + Object.entries(manifest.viewByID ?? {}).forEach(([viewID, view]) => { if (view.config) { - deserialize(viewID, view.config, dataIDMap); + deserialize(viewID, view.config, configIDMap); } }); }; diff --git a/src/store/view-configs/common.ts b/src/store/view-configs/common.ts index c4a67dc0d..e52c6ab60 100644 --- a/src/store/view-configs/common.ts +++ b/src/store/view-configs/common.ts @@ -20,6 +20,11 @@ const serializeViewConfig = < const views = Object.values(viewByID); views.forEach((view) => { + const savedConfigs = view.config ?? {}; + Object.entries(savedConfigs).forEach(([dataID, config]) => { + delete config[viewConfigStateKey]; + if (!Object.keys(config).length) delete savedConfigs[dataID]; + }); dataIDs.forEach((dataID) => { const viewConfig = viewConfigs[view.id]?.[dataID]; if (viewConfig !== undefined) { diff --git a/src/store/view-configs/layers.ts b/src/store/view-configs/layers.ts index 9016d52f7..f9c0393b4 100644 --- a/src/store/view-configs/layers.ts +++ b/src/store/view-configs/layers.ts @@ -1,9 +1,4 @@ import vtkPiecewiseFunctionProxy from '@kitware/vtk.js/Proxy/Core/PiecewiseFunctionProxy'; -import { - getColorFunctionRangeFromPreset, - getOpacityFunctionFromPreset, -} from '@/src/utils/vtk-helpers'; -import { ColorTransferFunction } from '@/src/types/views'; import { defineStore } from 'pinia'; import { reactive } from 'vue'; import { @@ -13,93 +8,162 @@ import { patchDoubleKeyRecord, } from '@/src/utils/doubleKeyRecord'; import { Maybe } from '@/src/types'; -import { identity } from '@/src/utils'; -import { LAYER_PRESET_BY_MODALITY, LAYER_PRESET_DEFAULT } from '@/src/config'; -import { isDicomImage } from '@/src/utils/dataSelection'; +import { resolveLayerColorPreset } from '@/src/vtk/LayerColorPresets'; import { useImageCacheStore } from '@/src/store/image-cache'; -import { createViewConfigSerializer } from './common'; -import { ViewConfig } from '../../io/state-file/schema'; -import { LayersConfig } from './types'; +import DicomChunkImage from '@/src/core/streaming/dicomChunkImage'; +import { getConsistentDicomWindowRange } from '@/src/utils/dicomWindowRange'; +import { DEFAULT_PRESET_BY_MODALITY } from '@/src/config'; +import { + getColorFunctionRangeFromPreset, + getOpacityFunctionFromPreset, +} from '@/src/utils/vtk-helpers'; +import { StateFile, ViewConfig } from '../../io/state-file/schema'; +import { LayerDisplayConfig, LayersConfig } from './types'; import { useLayersStore } from '../datasets-layers'; -import { useDICOMStore } from '../datasets-dicom'; - -function getPreset(id: string) { - const layersStore = useLayersStore(); - const layer = layersStore.getLayer(id); - if (!layer) { - // A layer can be gone by the time its preset is asked for. - return LAYER_PRESET_DEFAULT; - } - - if (isDicomImage(layer.selection)) { - const dicomStore = useDICOMStore(); - const { Modality = undefined } = dicomStore.volumeInfo[layer.selection]; - return ( - (Modality && LAYER_PRESET_BY_MODALITY[Modality]) || LAYER_PRESET_DEFAULT - ); - } - - return LAYER_PRESET_DEFAULT; -} const defaultLayersConfig = (): LayersConfig => ({ - colorBy: { - arrayName: '', - location: 'pointData', - }, transferFunction: { - preset: '', + preset: resolveLayerColorPreset(), mappingRange: [0, 1], }, - // opacity function not used in VtkTwoView opacityFunction: { - mode: vtkPiecewiseFunctionProxy.Mode.Gaussians, - gaussians: [], + mode: vtkPiecewiseFunctionProxy.Mode.Nodes, + nodes: [ + { x: 0, y: 1, midpoint: 0.5, sharpness: 0 }, + { x: 1, y: 1, midpoint: 0.5, sharpness: 0 }, + ], mappingRange: [0, 1], }, blendConfig: { opacity: 0.3, visibility: true }, + transparentBackground: true, +}); + +const defaultMulticomponentConfig = ( + imageDataRange: [number, number], + modality?: string | null +): LayersConfig => { + const preset = + (modality && DEFAULT_PRESET_BY_MODALITY[modality]) || '2hot-opaque'; + return { + ...defaultLayersConfig(), + transferFunction: { + preset, + mappingRange: getColorFunctionRangeFromPreset(preset) ?? imageDataRange, + }, + opacityFunction: { + ...getOpacityFunctionFromPreset(preset), + mappingRange: imageDataRange, + }, + }; +}; + +const normalizeConfig = (config: LayersConfig): LayersConfig => ({ + ...config, + transferFunction: { + ...config.transferFunction, + preset: resolveLayerColorPreset(config.transferFunction.preset), + }, +}); + +const displayFromConfig = (config: LayersConfig): LayerDisplayConfig => ({ + preset: resolveLayerColorPreset(config.transferFunction.preset), + mappingRange: [...config.transferFunction.mappingRange], + opacity: config.blendConfig.opacity, + visibility: config.blendConfig.visibility ?? true, + transparentBackground: config.transparentBackground ?? true, +}); + +const applyDisplayPatch = ( + config: LayersConfig, + display: Partial +): LayersConfig => ({ + ...config, + transparentBackground: + display.transparentBackground ?? config.transparentBackground ?? true, + transferFunction: { + ...config.transferFunction, + preset: resolveLayerColorPreset( + display.preset ?? config.transferFunction.preset + ), + mappingRange: [ + ...(display.mappingRange ?? config.transferFunction.mappingRange), + ], + }, + blendConfig: { + ...config.blendConfig, + opacity: display.opacity ?? config.blendConfig.opacity, + visibility: display.visibility ?? config.blendConfig.visibility ?? true, + }, }); export const useLayerColoringStore = defineStore('layerColoring', () => { const imageCacheStore = useImageCacheStore(); const configs = reactive>({}); + const displayConfigs = reactive>({}); const computeDefaultConfig = (dataID: string): LayersConfig => { const defaults = defaultLayersConfig(); - const preset = getPreset(dataID); const image = imageCacheStore.getVtkImageData(dataID); if (!image) return defaults; - const imageDataRange = image.getPointData().getScalars().getRange(); + const scalars = image.getPointData().getScalars(); + const imageDataRange = scalars.getRange(); + const scalarImage = scalars.getNumberOfComponents() === 1; - const ctRange = getColorFunctionRangeFromPreset(preset); - const ctFunc: Partial = { - preset, - mappingRange: ctRange || imageDataRange, - }; - - const opacityFunction = { - ...getOpacityFunctionFromPreset(preset), - mappingRange: imageDataRange, - }; + const layer = useLayersStore().getLayer(dataID); + const source = layer && imageCacheStore.imageById[layer.selection]; + if (!scalarImage) { + const modality = + source instanceof DicomChunkImage ? source.getModality() : undefined; + return defaultMulticomponentConfig(imageDataRange, modality); + } + const isPET = + source instanceof DicomChunkImage && source.getModality() === 'PT'; + const dicomRange = isPET + ? getConsistentDicomWindowRange( + source.getChunks().map((chunk) => chunk.metadata) + ) + : undefined; return { ...defaults, - transferFunction: { ...defaults.transferFunction, ...ctFunc }, - opacityFunction, + transferFunction: { + ...defaults.transferFunction, + mappingRange: dicomRange ?? imageDataRange, + }, + blendConfig: { + ...defaults.blendConfig, + opacity: isPET ? 0.5 : defaults.blendConfig.opacity, + }, }; }; + const getDisplayConfig = (dataID: string) => { + const current = displayConfigs[dataID]; + if (current) return current; + const display = displayFromConfig(computeDefaultConfig(dataID)); + // Loading placeholders must not become the defaults for future views. + if (imageCacheStore.getVtkImageData(dataID)) { + displayConfigs[dataID] = display; + return displayConfigs[dataID]; + } + return display; + }; + const getConfig = (viewID: Maybe, dataID: Maybe) => { if (!viewID || !dataID) return defaultLayersConfig(); const config = getDoubleKeyRecord(configs, viewID, dataID); if (config) return config; - // If no config exists, compute and store the default with proper preset - const defaultConfig = computeDefaultConfig(dataID); - patchDoubleKeyRecord(configs, viewID, dataID, defaultConfig); + const defaultConfig = applyDisplayPatch( + computeDefaultConfig(dataID), + getDisplayConfig(dataID) + ); + if (imageCacheStore.getVtkImageData(dataID)) { + patchDoubleKeyRecord(configs, viewID, dataID, defaultConfig); + } return defaultConfig; }; @@ -108,59 +172,36 @@ export const useLayerColoringStore = defineStore('layerColoring', () => { dataID: string, patch: Partial ) => { - const config = { + const config = normalizeConfig({ ...defaultLayersConfig(), ...getConfig(viewID, dataID), ...patch, - }; + }); patchDoubleKeyRecord(configs, viewID, dataID, config); }; - const createUpdateFunc = ( - key: K, - transform: (config: LayersConfig[K]) => LayersConfig[K] = identity + const updateDisplayConfig = ( + dataID: string, + patch: Partial ) => { - return ( - viewID: string, - dataID: string, - update: Partial - ) => { - const config = getConfig(viewID, dataID) ?? defaultLayersConfig(); - const updatedConfig = transform({ - ...config[key], - ...update, - }); - updateConfig(viewID, dataID, { [key]: updatedConfig }); - }; - }; - - const updateColorBy = createUpdateFunc('colorBy'); - const updateColorTransferFunction = createUpdateFunc('transferFunction'); - const updateOpacityFunction = createUpdateFunc('opacityFunction'); - const updateBlendConfig = createUpdateFunc('blendConfig'); - - const setColorPreset = (viewID: string, layerID: string, preset: string) => { - const image = imageCacheStore.getVtkImageData(layerID); - if (!image) return; - const imageDataRange = image.getPointData().getScalars().getRange(); - - const ctRange = getColorFunctionRangeFromPreset(preset); - const ctFunc: Partial = { - preset, - mappingRange: ctRange || imageDataRange, - }; - updateColorTransferFunction(viewID, layerID, ctFunc); - - const opFunc = { - ...getOpacityFunctionFromPreset(preset), - mappingRange: imageDataRange, + const current = getDisplayConfig(dataID); + displayConfigs[dataID] = { + ...current, + ...patch, + preset: resolveLayerColorPreset(patch.preset ?? current.preset), + visibility: patch.visibility ?? current.visibility ?? true, + transparentBackground: + patch.transparentBackground ?? current.transparentBackground ?? true, + mappingRange: patch.mappingRange + ? [...patch.mappingRange] + : current.mappingRange, }; - updateOpacityFunction(viewID, layerID, opFunc); - }; - - const resetColorPreset = (viewID: string, layerID: string) => { - setColorPreset(viewID, layerID, getPreset(layerID)); + Object.entries(configs).forEach(([viewID, viewConfigs]) => { + const config = viewConfigs[dataID]; + if (config) + updateConfig(viewID, dataID, applyDisplayPatch(config, patch)); + }); }; const removeView = (viewID: string) => { @@ -172,14 +213,66 @@ export const useLayerColoringStore = defineStore('layerColoring', () => { delete configs[viewID]?.[dataID]; } else { deleteSecondKey(configs, dataID); + delete displayConfigs[dataID]; } }; - const serialize = createViewConfigSerializer(configs, 'layers'); + const serialize = ({ manifest }: StateFile) => { + const dataIDs = new Set((manifest.datasets ?? []).map(({ id }) => id)); + + const layerIDs = Object.entries(useLayersStore().parentToLayers).flatMap( + ([parent, layers]) => + dataIDs.has(parent) + ? layers + .filter(({ selection }) => dataIDs.has(selection)) + .map(({ id }) => id) + : [] + ); + + manifest.layerDisplay = {}; + layerIDs.forEach((layerID) => { + const display = displayConfigs[layerID]; + if (display) { + manifest.layerDisplay![layerID] = { + ...display, + mappingRange: [...display.mappingRange], + }; + } + }); + + Object.values(manifest.viewByID ?? {}).forEach((view) => { + // Saved view records are shared with the live store across saves. + const savedConfigs = view.config ?? {}; + Object.entries(savedConfigs).forEach(([dataID, config]) => { + delete config.layers; + if (!Object.keys(config).length) delete savedConfigs[dataID]; + }); + layerIDs.forEach((layerID) => { + const config = configs[view.id]?.[layerID]; + if (!config) return; + view.config ??= {}; + view.config[layerID] ??= {}; + view.config[layerID].layers = config; + }); + }); + }; + + const deserializeDisplayConfigs = ( + saved: Record, + dataIDMap: Record + ) => { + Object.entries(saved).forEach(([dataID, display]) => { + const mappedID = dataIDMap[dataID]; + if (mappedID) { + updateDisplayConfig(mappedID, display); + } + }); + }; const deserialize = (viewID: string, config: Record) => { Object.entries(config).forEach(([dataID, viewConfig]) => { if (viewConfig.layers) { + displayConfigs[dataID] ??= displayFromConfig(viewConfig.layers); updateConfig(viewID, dataID, viewConfig.layers); } }); @@ -187,18 +280,15 @@ export const useLayerColoringStore = defineStore('layerColoring', () => { return { configs, + displayConfigs, + getDisplayConfig, + updateDisplayConfig, getConfig, - updateConfig, - updateColorBy, - updateColorTransferFunction, - updateOpacityFunction, - updateBlendConfig, - setColorPreset, - resetColorPreset, removeView, removeData, serialize, deserialize, + deserializeDisplayConfigs, }; }); diff --git a/src/store/view-configs/types.ts b/src/store/view-configs/types.ts index 6503e2fbd..582b65ded 100644 --- a/src/store/view-configs/types.ts +++ b/src/store/view-configs/types.ts @@ -34,6 +34,8 @@ export interface VolumeColorConfig { } export interface WindowLevelConfig { + preset?: string; + visibility?: boolean; width?: number; level?: number; auto: keyof typeof WLAutoRanges; // User-selected percentile range @@ -41,15 +43,16 @@ export interface WindowLevelConfig { userTriggered?: boolean; // Whether the user has changed the window/level } -export interface LayersConfig { - colorBy: { - arrayName: string; - location: string; - }; +export type LayersConfig = { transferFunction: ColorTransferFunction; opacityFunction: OpacityFunction; blendConfig: BlendConfig; -} + transparentBackground: boolean; +}; + +export type LayerDisplayConfig = ColorTransferFunction & + Pick & + Pick; export interface CinePlaybackViewConfig { frame: number; diff --git a/src/store/view-configs/windowing.ts b/src/store/view-configs/windowing.ts index da3ecdc22..dbc6ecc0e 100644 --- a/src/store/view-configs/windowing.ts +++ b/src/store/view-configs/windowing.ts @@ -1,4 +1,5 @@ import { defineStore } from 'pinia'; +import { resolveBaseColorPreset } from '@/src/vtk/LayerColorPresets'; import { markRaw, reactive, ref } from 'vue'; import { createEventHook } from '@vueuse/core'; import { @@ -26,8 +27,15 @@ const minMaxToWidthLevel = (min: number, max: number) => ({ level: (max + min) / 2, }); +const withAppearanceDefaults = (config: WindowLevelConfig) => ({ + ...config, + preset: resolveBaseColorPreset(config.preset), + visibility: config.visibility ?? true, +}); + export const defaultWindowLevelConfig = () => ({ + visibility: true, width: 1, level: 0.5, auto: WL_AUTO_DEFAULT, @@ -36,7 +44,10 @@ export const defaultWindowLevelConfig = () => }) as WindowLevelConfig; export const useWindowingStore = defineStore('windowing', () => { - const configs = reactive>({}); + const configs = reactive< + DoubleKeyRecord> + >({}); + const runtimeConfigWindowLevel = ref(); const imageStatsStore = useImageStatsStore(); @@ -75,12 +86,13 @@ export const useWindowingStore = defineStore('windowing', () => { }; const getConfig = (viewID: string, dataID: string): WindowLevelConfig => { - const internalConfig = + const internalConfig = withAppearanceDefaults( getDoubleKeyRecord(configs, viewID, dataID) ?? - computeDefaultConfig(dataID); + computeDefaultConfig(dataID) + ); if (!internalConfig.useAuto) { - return { ...internalConfig }; + return internalConfig; } const autoKey = internalConfig.auto; @@ -92,7 +104,7 @@ export const useWindowingStore = defineStore('windowing', () => { ...minMaxToWidthLevel(min, max), }; } - return { ...internalConfig }; + return internalConfig; }; const getInternalConfig = (viewID: Maybe, dataID: Maybe) => @@ -105,7 +117,7 @@ export const useWindowingStore = defineStore('windowing', () => { userTriggered = false ) => { const currentInternalConfig = getInternalConfig(viewID, dataID); - const defaults = defaultWindowLevelConfig(); + const defaults = computeDefaultConfig(dataID); let effectiveUseAuto = currentInternalConfig?.useAuto ?? defaults.useAuto; let widthLevelPatchOnSwitchingFromAuto; @@ -131,7 +143,7 @@ export const useWindowingStore = defineStore('windowing', () => { // Ensure we always have required fields from defaults const baseConfig = currentInternalConfig || defaults; - const newInternalConfig = { + const newInternalConfig = withAppearanceDefaults({ ...baseConfig, ...widthLevelPatchOnSwitchingFromAuto, ...patch, @@ -142,7 +154,7 @@ export const useWindowingStore = defineStore('windowing', () => { currentInternalConfig?.userTriggered || userTriggered || patch.userTriggered, - }; + }); patchDoubleKeyRecord(configs, viewID, dataID, newInternalConfig); diff --git a/src/store/views.ts b/src/store/views.ts index b1039afea..5d4d7fc96 100644 --- a/src/store/views.ts +++ b/src/store/views.ts @@ -206,7 +206,9 @@ export const useViewStore = defineStore('view', () => { } function addView(viewInit: ViewInfoInit) { - const id = idStore.nextId(); + let id = idStore.nextId(); + // Restored views retain their saved IDs without advancing the ID generator. + while (id in viewByID) id = idStore.nextId(); const view = { id, diff --git a/src/utils/__tests__/dicomWindowRange.spec.ts b/src/utils/__tests__/dicomWindowRange.spec.ts new file mode 100644 index 000000000..4481cbcd4 --- /dev/null +++ b/src/utils/__tests__/dicomWindowRange.spec.ts @@ -0,0 +1,97 @@ +import { describe, expect, it } from 'vitest'; +import { Tags } from '@/src/core/dicomTags'; +import { getConsistentDicomWindowRange } from '@/src/utils/dicomWindowRange'; + +function windowTags(center: string, width: string, voiFunction?: string) { + return Object.entries({ + [Tags.WindowLevel]: center, + [Tags.WindowWidth]: width, + ...(voiFunction === undefined + ? {} + : { [Tags.VOILUTFunction]: voiFunction }), + }); +} + +describe('DICOM window display range', () => { + it.each([ + ['unsigned identity', '2048', '4096', undefined, [0, 4095]], + ['signed window', '0', '100', 'LINEAR', [-50, 49]], + ['fractional exact window', '0.5', '1', 'LINEAR_EXACT', [0, 1]], + ['sub-unit exact width', '0', '0.25', 'LINEAR_EXACT', [-0.125, 0.125]], + ] as const)( + 'uses the standard boundaries for %s', + (_name, center, width, voiFunction, expected) => { + expect( + getConsistentDicomWindowRange([windowTags(center, width, voiFunction)]) + ).toEqual(expected); + } + ); + + it('compares parsed values across chunks and defaults an absent function to LINEAR', () => { + expect( + getConsistentDicomWindowRange([ + windowTags('1000', '2000'), + windowTags(' +1e3 ', '2.000E+3', ' LINEAR '), + windowTags('1000.0', '2000.', ''), + ]) + ).toEqual([0, 1999]); + }); + + it('chooses the first paired alternative without mixing centers and widths', () => { + expect( + getConsistentDicomWindowRange([ + windowTags('50\\500', '100\\1000'), + windowTags('50.0\\750', '1e2\\1500'), + ]) + ).toEqual([0, 99]); + }); + + it.each([ + ['no chunks', []], + ['missing first metadata', [undefined, windowTags('50', '100')]], + ['missing later metadata', [windowTags('50', '100'), null]], + ['missing center', [[[Tags.WindowWidth, '100']]]], + ['missing width', [[[Tags.WindowLevel, '50']]]], + ['different centers', [windowTags('50', '100'), windowTags('51', '100')]], + ['different widths', [windowTags('50', '100'), windowTags('50', '101')]], + [ + 'different functions', + [windowTags('50', '100'), windowTags('50', '100', 'LINEAR_EXACT')], + ], + ] as const)('rejects %s', (_name, metadata) => { + expect(getConsistentDicomWindowRange(metadata)).toBeUndefined(); + }); + + it.each([ + ['empty center', '', '100', undefined], + ['blank center', ' ', '100', undefined], + ['empty width', '50', '', undefined], + ['blank width', '50', ' ', undefined], + ['incomplete exponent', '1e-', '100', undefined], + ['trailing center text', '50abc', '100', undefined], + ['trailing width text', '50', '100abc', undefined], + ['hexadecimal center', '0x10', '100', undefined], + ['infinite center', 'Infinity', '100', undefined], + ['infinite width', '50', 'Infinity', undefined], + ['nonfinite decimal', '1e999', '100', undefined], + ['NaN center', 'NaN', '100', undefined], + ['zero width', '50', '0', undefined], + ['negative width', '50', '-100', undefined], + ['LINEAR threshold', '50', '1', undefined], + ['sub-unit LINEAR width', '50', '0.5', 'LINEAR'], + ['zero exact width', '50', '0', 'LINEAR_EXACT'], + ['SIGMOID function', '50', '100', 'SIGMOID'], + ['unknown function', '50', '100', 'CUSTOM'], + ['mismatched alternatives', '50\\500', '100', undefined], + ['blank alternative', '50\\', '100\\1000', undefined], + ['malformed alternative', '50\\bad', '100\\1000', undefined], + ['nonpositive alternative', '50\\500', '100\\0', undefined], + ['overflowing endpoint', '1.7e308', '1e308', 'LINEAR_EXACT'], + ['rounded equal endpoints', '1e20', '2', 'LINEAR_EXACT'], + ['underflowing half-width', '0', '5e-324', 'LINEAR_EXACT'], + ] as const)('rejects %s', (_name, center, width, voiFunction) => { + expect( + getConsistentDicomWindowRange([windowTags(center, width, voiFunction)]) + ).toBeUndefined(); + }); +}); diff --git a/src/utils/dicomWindowRange.ts b/src/utils/dicomWindowRange.ts new file mode 100644 index 000000000..8dd7455aa --- /dev/null +++ b/src/utils/dicomWindowRange.ts @@ -0,0 +1,57 @@ +import { Tags } from '@/src/core/dicomTags'; + +type DicomMetadata = Iterable | null | undefined; + +const decimal = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/; + +const parseValues = (value?: string) => + value + ?.split('\\') + .map((token) => (decimal.test(token.trim()) ? Number(token) : NaN)) ?? []; + +function readWindow(metadata: DicomMetadata) { + if (!metadata) return undefined; + const tags = Object.fromEntries(metadata); + const centers = parseValues(tags[Tags.WindowLevel]); + const widths = parseValues(tags[Tags.WindowWidth]); + const voiFunction = + tags[Tags.VOILUTFunction]?.trim().toUpperCase() || 'LINEAR'; + if ( + !centers.length || + centers.length !== widths.length || + !centers.every(Number.isFinite) || + !widths.every((width) => Number.isFinite(width) && width > 0) || + !['LINEAR', 'LINEAR_EXACT'].includes(voiFunction) + ) + return undefined; + return { center: centers[0], width: widths[0], voiFunction }; +} + +export function getConsistentDicomWindowRange( + metadata: readonly DicomMetadata[] +): [number, number] | undefined { + const windows = metadata.map(readWindow); + const first = windows[0]; + if ( + !first || + !windows.every( + (window) => + window?.center === first.center && + window.width === first.width && + window.voiFunction === first.voiFunction + ) + ) + return undefined; + // DICOM LINEAR has half-value offsets; LINEAR_EXACT uses the full width. + const offset = first.voiFunction === 'LINEAR' ? 1 : 0; + const center = first.center - offset / 2; + const halfWidth = (first.width - offset) / 2; + const min = center - halfWidth; + const max = center + halfWidth; + return Number.isFinite(min) && + Number.isFinite(max) && + min < max && + Number.isFinite(max - min) + ? [min, max] + : undefined; +} diff --git a/src/utils/layers.ts b/src/utils/layers.ts new file mode 100644 index 000000000..95ffd3bb8 --- /dev/null +++ b/src/utils/layers.ts @@ -0,0 +1,2 @@ +export const getLayerId = (parent: string, source: string) => + `${parent}::${source}`; diff --git a/src/utils/nativeColorWindow.ts b/src/utils/nativeColorWindow.ts new file mode 100644 index 000000000..5e357d563 --- /dev/null +++ b/src/utils/nativeColorWindow.ts @@ -0,0 +1,17 @@ +const nativeColorWidths: Record = { + Uint8Array: 255, + Uint8ClampedArray: 255, + Uint16Array: 65535, + Uint32Array: 4294967295, + Int8Array: 127, + Int16Array: 32767, + Int32Array: 2147483647, + Float32Array: 1, + Float64Array: 1, +}; + +export function nativeColorWindow(dataType: string | undefined) { + // Native channels use a zero-based integer range or normalized floats. + const width = nativeColorWidths[dataType ?? 'Uint8Array'] ?? 255; + return { width, level: width / 2 }; +} diff --git a/tests/baseline/layers_max_opacity_2d_view-chrome-1.png b/tests/baseline/layers_max_opacity_2d_view-chrome-1.png deleted file mode 100644 index d21362f575d4252563abcf577471674d7466591a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 217013 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

MJAZF=B}1S7e243RK`d-tfBr~4 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 P{6kUFIFqkn>i_=$Oiw)m diff --git a/tests/specs/base-image-availability.e2e.ts b/tests/specs/base-image-availability.e2e.ts new file mode 100644 index 000000000..f6c3754fe --- /dev/null +++ b/tests/specs/base-image-availability.e2e.ts @@ -0,0 +1,94 @@ +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { volViewPage } from '../pageobjects/volview.page'; +import { CINE_VIEW_SELECTOR } from './cineTestUtils'; +import { buildSyntheticCineDicom, newUid } from './syntheticDicom'; +import { + captureViewPixels, + makeTempDir, + openVolViewPage, + waitForModuleTransitions, +} from './utils'; + +async function selectCineLayout(name: string, count: number) { + await volViewPage.layoutButton.click(); + const option = $(`.v-list-item-title=${name}`); + await option.waitForClickable(); + await option.click(); + await expect($$(CINE_VIEW_SELECTOR)).toBeElementsArrayOfSize(count); +} + +async function expectCineVisibility(visible: boolean, count: number) { + await $('header').moveTo(); + await browser.waitUntil( + async () => { + const views = await captureViewPixels( + CINE_VIEW_SELECTOR, + 80, + 'cine-base-visibility.png' + ); + return ( + views.length === count && + views.every(({ data }) => { + const bright = data.filter( + (value, index) => index % 4 !== 3 && value > 40 + ).length; + return visible ? bright > 1000 : bright < 1000; + }) + ); + }, + { + timeoutMsg: `Expected ${count} cine images to be ${visible ? 'visible' : 'hidden'}`, + } + ); +} + +describe('Base image availability', () => { + it('keeps cine visibility available in 3D Only and maximized 3D slots', async () => { + const directory = makeTempDir('cine-base-visibility'); + writeFileSync( + join(directory, 'cine.dcm'), + buildSyntheticCineDicom({ + studyUid: newUid(), + seriesUid: newUid(), + sopUid: newUid(), + rows: 16, + cols: 16, + numberOfFrames: 3, + }) + ); + await openVolViewPage('cine-base-visibility/cine.dcm'); + await selectCineLayout('Four Up', 4); + await volViewPage.renderingModuleTab.click(); + await waitForModuleTransitions(); + const visibility = $('[data-testid="base-image-visibility"]'); + await visibility.scrollIntoView({ block: 'center', behavior: 'instant' }); + await expect(visibility).toBeEnabled(); + await expectCineVisibility(true, 4); + await visibility.click(); + await expect(visibility).toHaveText(expect.stringContaining('SHOW')); + await expectCineVisibility(false, 4); + + await selectCineLayout('3D Only', 1); + await expect(visibility).toHaveText(expect.stringContaining('SHOW')); + await expect(visibility).toBeEnabled(); + await expect( + $('[data-testid="base-image-range-min"] input') + ).toBeDisabled(); + await expect($('[data-testid="base-image-contrast"]')).toBeDisabled(); + await expectCineVisibility(false, 1); + await visibility.click(); + await expect(visibility).toHaveText(expect.stringContaining('HIDE')); + await expectCineVisibility(true, 1); + + await selectCineLayout('Four Up', 4); + await visibility.click(); + await expect(visibility).toHaveText(expect.stringContaining('SHOW')); + await $$(CINE_VIEW_SELECTOR)[3].doubleClick(); + await expect($$(CINE_VIEW_SELECTOR)).toBeElementsArrayOfSize(1); + await expect(visibility).toBeEnabled(); + await expectCineVisibility(false, 1); + await visibility.click(); + await expectCineVisibility(true, 1); + }); +}); diff --git a/tests/specs/base-image-display.e2e.ts b/tests/specs/base-image-display.e2e.ts new file mode 100644 index 000000000..53e8ac789 --- /dev/null +++ b/tests/specs/base-image-display.e2e.ts @@ -0,0 +1,585 @@ +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { TEMP_DIR } from '../../wdio.shared.conf'; +import { setValueVueInput, volViewPage } from '../pageobjects/volview.page'; +import { + capture2DViewScreenshotMetrics, + captureViewPixels, + makeTempDir, + openVolViewPage, + selectLayout, + SESSION_SAVE_TIMEOUT, + waitForDownload, + waitForModuleTransitions, + writeManifestToFile, + writeMetaImage, +} from './utils'; + +const field = (endpoint: 'min' | 'max') => + $(`[data-testid="base-image-range-${endpoint}"] input`); +const handle = (endpoint: 'min' | 'max' | 'center') => + $(`[data-testid="base-image-range-${endpoint}-handle"]`); + +async function openBase(withOverlay = false) { + const directory = makeTempDir('base-display'); + const header = [ + 'NRRD0005', + 'type: short', + 'dimension: 3', + 'sizes: 16 16 16', + 'space: left-posterior-superior', + 'space directions: (1,0,0) (0,1,0) (0,0,1)', + 'space origin: (0,0,0)', + 'encoding: ascii', + ].join('\n'); + // Every orthogonal plane contains a ramp, including negative CT values. + const voxels = Array.from({ length: 4096 }, (_, index) => { + const x = index % 16; + const y = Math.floor(index / 16) % 16; + const z = Math.floor(index / 256); + return -1000 + ((x + y + z) % 16) * 100; + }).join(' '); + writeFileSync(join(directory, 'base.nrrd'), `${header}\n\n${voxels}`); + await writeManifestToFile( + { + windowing: { width: 1500, level: -250 }, + io: { layerExtension: 'layer' }, + }, + 'base-display-config.json' + ); + const overlay = withOverlay ? writeMetaImage('base.layer.mha') : undefined; + await volViewPage.open( + `?urls=[tmp/base-display-config.json,tmp/base-display/base.nrrd${ + overlay ? `,tmp/${overlay}` : '' + }]` + ); + await volViewPage.waitForViews(); + await selectLayout('Four Up', 3, true); + await volViewPage.renderingModuleTab.click(); + for (const title of ['Color Presets', 'Cinematic Rendering']) { + const section = $(`.v-expansion-panel-title*=${title}`); + if ((await section.getAttribute('aria-expanded')) === 'true') + await section.click(); + } + await waitForModuleTransitions(); + await browser.waitUntil( + async () => (await $$('.Vue-Toastification__toast').length) === 0 + ); + await field('min').scrollIntoView({ block: 'center', behavior: 'instant' }); + await expect(field('min')).toBeEnabled(); +} + +async function expectRange(minimum: number, maximum: number) { + await expect(field('min')).toHaveAttribute('title', String(minimum)); + await expect(field('max')).toHaveAttribute('title', String(maximum)); + const label = `W/L: ${(maximum - minimum).toFixed(2)} / ${( + (maximum + minimum) / + 2 + ).toFixed(2)}`; + for (const view of await volViewPage.getViews2D()) + await expect(view).toHaveText(expect.stringContaining(label)); +} + +async function setRange(minimum: string, maximum: string) { + await setValueVueInput(field('min'), minimum); + await setValueVueInput(field('max'), maximum); + await browser.keys('Enter'); +} + +const capture = () => + capture2DViewScreenshotMetrics('base-display-capture.png'); + +async function expectViewsChanged(before: Awaited>) { + await $('header').moveTo(); + await browser.waitUntil( + async () => { + const after = await capture(); + return ( + after.length === 3 && + after.every( + ({ checksum }, index) => checksum !== before[index].checksum + ) + ); + }, + { timeoutMsg: 'Expected the base window to change all three slice images' } + ); +} + +async function openWindowing() { + await volViewPage.selectTool('mdi-circle-half-full'); + await $('.v-menu.v-overlay--active .manual-inputs input').waitForDisplayed(); + await $('.v-menu.v-overlay--active .manual-inputs').waitForStable(); +} + +async function expectRangeMatchesWindowing() { + const minimum = Number(await field('min').getAttribute('title')); + const maximum = Number(await field('max').getAttribute('title')); + await expectRange(minimum, maximum); + return [minimum, maximum] as const; +} + +async function selectBasePalette(title: string) { + const palette = $('[data-testid="base-image-colormap"]'); + await palette.scrollIntoView({ block: 'center', behavior: 'instant' }); + await palette.click(); + await $('.v-menu.v-overlay--active .v-overlay__content').waitForStable(); + const option = $(`[role="option"]*=${title}`); + await option.scrollIntoView({ block: 'nearest', behavior: 'instant' }); + await option.waitForStable(); + await option.click(); + await expect(palette).toHaveText(expect.stringContaining(title)); +} + +async function obliqueColors() { + const views = await captureViewPixels( + '.oblique-grid [data-testid="vtk-view"]', + 40, + 'base-oblique-capture.png' + ); + return views.map(({ data }) => { + let colored = 0; + for (let i = 0; i < data.length; i += 4) + if ( + Math.max(data[i], data[i + 1], data[i + 2]) - + Math.min(data[i], data[i + 1], data[i + 2]) > + 40 + ) + colored++; + return colored; + }); +} + +describe('Base image display range', () => { + it('edits a base without overlays and synchronizes presets, keyboard, drag and mouse windowing', async () => { + await openBase(); + await expect($('[data-testid="base-image-properties"]')).toBeDisplayed(); + await expect($('[data-testid="layer-colormap"]')).not.toExist(); + await expectRange(-1000, 500); + await $('header').moveTo(); + const original = await capture(); + await setRange('-800', '200'); + await expectRange(-800, 200); + await expectViewsChanged(original); + await setRange('50', '200'); + await setValueVueInput(field('min'), '300.000'); + await setValueVueInput(field('max'), '400.000'); + await expectRange(300, 400); + await expect(field('max')).toBeFocused(); + await expect(field('max')).toHaveValue('400.000'); + await openWindowing(); + const manual = $$('.v-overlay--active .manual-inputs input'); + await expect(manual[0]).toHaveValue('100'); + await expect(manual[1]).toHaveValue('350'); + await setValueVueInput(manual[0], '600'); + await setValueVueInput(manual[1], '-200'); + await browser.keys('Enter'); + await expectRange(-500, 100); + await expect(field('min')).toHaveValue('-500'); + await expect(field('max')).toHaveValue('100'); + await expect($('[data-testid="base-image-range-status"]')).not.toExist(); + await browser.keys('Escape'); + await setValueVueInput(field('min'), '-'); + await openWindowing(); + await $('.v-menu.v-overlay--active').$('label=Air').click(); + await expectRange(-926, 74); + await expect(field('min')).toHaveValue('-'); + await browser.keys('Escape'); + await setValueVueInput(field('min'), '0.1'); + await field('max').click(); + expect(Number(await field('min').getAttribute('title'))).toBeCloseTo( + 0.1, + 12 + ); + await openWindowing(); + await $('.v-menu.v-overlay--active').$('label=Air').click(); + await expectRange(-926, 74); + await expect(field('min')).toHaveValue('-926'); + await browser.keys('Escape'); + await field('min').scrollIntoView({ block: 'center', behavior: 'instant' }); + const gradient = await $( + '[data-testid="base-image-range-bar"] .color-map-swatch' + ).getAttribute('style'); + if (!gradient) throw new Error('Expected a base grayscale gradient'); + expect(gradient).toContain('linear-gradient('); + await setRange('-800', '200'); + await handle('max').execute((element) => element.focus()); + await browser.keys('ArrowDown'); + await expectRange(-800, 185); + await setRange('180', '185'); + await handle('min').execute((element) => element.focus()); + await expect(handle('min')).toBeFocused(); + await browser.keys('ArrowRight'); + for (const endpoint of ['min', 'max', 'center'] as const) + await expect(handle(endpoint)).toBeEnabled(); + const narrow = await expectRangeMatchesWindowing(); + expect(narrow[0]).toBeGreaterThan(184); + expect(narrow[0]).toBeLessThan(narrow[1]); + expect(narrow[1]).toBe(185); + await setRange('-995', '185'); + await handle('min').execute((element) => element.focus()); + await browser.keys('ArrowLeft'); + await expectRange(-1000, 185); + await setRange('-800', '-795'); + await handle('max').execute((element) => element.focus()); + await expect(handle('max')).toBeFocused(); + await browser.keys('ArrowDown'); + const narrowAtMin = await expectRangeMatchesWindowing(); + expect(narrowAtMin[0]).toBe(-800); + expect(narrowAtMin[1]).toBeGreaterThan(-800); + expect(narrowAtMin[1]).toBeLessThan(-799); + await setRange('-800', '50'); + const center = handle('center'); + const location = await center.getLocation(); + const size = await center.getSize(); + const x = Math.round(location.x + size.width / 2); + const y = Math.round(location.y + size.height / 2); + await browser + .action('pointer') + .move({ x, y }) + .down() + .move({ x: x + 15, y }) + .up() + .perform(); + const dragged = await expectRangeMatchesWindowing(); + expect(dragged[0]).toBeGreaterThan(-800); + expect(dragged[1] - dragged[0]).toBeCloseTo(850, 8); + await expect( + $('[data-testid="base-image-range-bar"] .color-map-swatch') + ).not.toHaveAttribute('style', gradient); + + await setRange('100', '200'); + await setValueVueInput(field('max'), '99'); + await browser.keys('Enter'); + await expect($('[data-testid="base-image-range-status"]')).toHaveText( + 'Min must be less than Max.' + ); + await expectRange(100, 200); + await setValueVueInput(field('max'), '200'); + await browser.keys('Enter'); + await setRange('-800', '200'); + const view = (await volViewPage.getViews2D())[0]; + const canvas = await view.$('canvas'); + const origin = await canvas.getLocation(); + const bounds = await canvas.getSize(); + const cx = Math.round(origin.x + bounds.width / 2); + const cy = Math.round(origin.y + bounds.height / 2); + await browser + .action('pointer') + .move({ x: cx, y: cy }) + .down() + .move({ x: cx + 30, y: cy + 20 }) + .up() + .perform(); + await browser.waitUntil( + async () => Number(await field('min').getAttribute('title')) !== -800 + ); + const mouseRange = await expectRangeMatchesWindowing(); + expect(mouseRange).not.toEqual([-800, 200]); + }); + + it('colors the base slices independently of windowing and volume rendering', async () => { + await openBase(); + await expect($('[data-testid="base-image-colormap"]')).toHaveText( + expect.stringContaining('Grayscale') + ); + await expect($('[data-testid="layer-colormap"]')).not.toExist(); + await $('header').moveTo(); + const before = await capture(); + const volumeSettings = await $$('.mapping-range-editor input').map( + (input) => input.getValue() + ); + await selectBasePalette('DICOM Hot Iron'); + await expectRange(-1000, 500); + await expectViewsChanged(before); + await browser.waitUntil(async () => + (await capture()).every(({ colored }) => colored > 1000) + ); + expect( + await $$('.mapping-range-editor input').map((input) => input.getValue()) + ).toEqual(volumeSettings); + await $('[data-testid="base-image-contrast"]').click(); + await $('.v-menu.v-overlay--active .v-card').waitForStable(); + await $('.v-menu.v-overlay--active').$('label=Air').click(); + await browser.keys('Escape'); + await expectRange(-926, 74); + await expect($('[data-testid="base-image-colormap"]')).toHaveText( + expect.stringContaining('DICOM Hot Iron') + ); + }); + + it('uses local Contrast presets to recover an offscreen window without snapping a drag', async () => { + await openBase(); + const volume = await volViewPage.getView3D(); + await volume!.click(); + const volumeBorder = () => + volume!.execute( + (element) => + getComputedStyle(element.closest('.grid-item')!).borderColor + ); + expect(await volumeBorder()).toBe('rgb(60, 179, 113)'); + const contrast = $('[data-testid="base-image-contrast"]'); + await expect(contrast).toBeDisplayed(); + await expect(contrast.$('.mdi-menu-down')).toExist(); + const minimumRect = await field('min').getLocation(); + const maximumRect = await field('max').getLocation(); + const contrastRect = await contrast.getLocation(); + expect(contrastRect.x).toBeGreaterThan(minimumRect.x); + expect(contrastRect.x).toBeLessThan(maximumRect.x); + await contrast.click(); + await $('.v-menu.v-overlay--active .v-card').waitForStable(); + await expectRange(-1000, 500); + const popup = $('.v-menu.v-overlay--active'); + await popup.$('label=Lungs').click(); + await expectRange(-1350, 150); + expect(await volumeBorder()).toBe('rgb(60, 179, 113)'); + await browser.keys('Escape'); + await expect(handle('min').$('.handle-grip')).toHaveAttribute( + 'data-direction', + 'left' + ); + const thumb = handle('min'); + const location = await thumb.getLocation(); + const size = await thumb.getSize(); + const x = Math.round(location.x + size.width / 2); + const y = Math.round(location.y + size.height / 2); + const trackWidth = ( + await $('[data-testid="base-image-range-bar"]').getSize() + ).width; + await browser + .action('pointer') + .move({ x, y }) + .down() + .move({ x: x + 2, y }) + .up() + .perform(); + const dragged = await expectRangeMatchesWindowing(); + expect(dragged[0]).toBeCloseTo(-1350 + (2 / trackWidth) * 1500, 8); + expect(dragged[0]).toBeLessThan(-1000); + expect(dragged[1]).toBeCloseTo(150, 8); + + await contrast.click(); + await $('.v-menu.v-overlay--active .v-card').waitForStable(); + expect(await expectRangeMatchesWindowing()).toEqual(dragged); + const manual = $$('.v-menu.v-overlay--active .manual-inputs input'); + await $('.v-menu.v-overlay--active .manual-inputs').waitForStable(); + await setValueVueInput(manual[0], '100'); + await setValueVueInput(manual[1], '-2000'); + await browser.keys('Enter'); + await expectRange(-2050, -1950); + await browser.keys('Escape'); + await expect(handle('center')).toBeEnabled(); + await expect(handle('center').$('.handle-grip')).toHaveAttribute( + 'data-direction', + 'left' + ); + await contrast.click(); + await $('.v-menu.v-overlay--active .v-card').waitForStable(); + await expectRange(-2050, -1950); + await $('.v-menu.v-overlay--active').$('label=Full Range').click(); + await expectRange(-1000, 500); + await browser.keys('Escape'); + await expect(field('min')).toHaveValue('-1000'); + await expect(field('max')).toHaveValue('500'); + for (const draft of ['-', '']) { + await setValueVueInput(field('min'), draft); + await browser.keys('Enter'); + await expect( + $('[data-testid="base-image-range-status"]') + ).toBeDisplayed(); + await contrast.click(); + await $('.v-menu.v-overlay--active .v-card').waitForStable(); + await $('.v-menu.v-overlay--active').$('label=Full Range').click(); + await browser.keys('Escape'); + await expect(field('min')).toHaveValue('-1000'); + await expect(field('max')).toHaveValue('500'); + await expect($('[data-testid="base-image-range-status"]')).not.toExist(); + } + }); + + it('shares base palette, range, visibility and reset with oblique views', async () => { + await openBase(); + await selectBasePalette('DICOM Hot Iron'); + await setRange('-800', '200'); + await volViewPage.layoutButton.click(); + await $('.v-menu.v-overlay--active .v-overlay__content').waitForStable(); + await $('.v-list-item-title=Oblique').click(); + await $('.oblique-grid').waitForDisplayed(); + await $('.oblique-grid').waitForStable(); + await expect(field('min')).toBeEnabled(); + await expectRange(-800, 200); + for (const view of await $$( + '.oblique-grid > .d-flex:not(:nth-child(2)) [data-testid="vtk-view"]' + )) + await expect(view).toHaveText( + expect.stringContaining('W/L: 1000.00 / -300.00') + ); + await $('header').moveTo(); + await browser.waitUntil(async () => { + const colors = await obliqueColors(); + return colors.length === 4 && colors.every((count) => count > 1000); + }); + const colored = await obliqueColors(); + + const expectUncolored = async () => { + await $('header').moveTo(); + await browser.waitUntil(async () => { + const colors = await obliqueColors(); + return ( + colors.length === 4 && + colors.every((count, i) => count < colored[i] / 3) + ); + }); + }; + + const expectColored = async () => { + await $('header').moveTo(); + await browser.waitUntil(async () => { + const colors = await obliqueColors(); + return ( + colors.length === 4 && + colors.every((count, i) => count > colored[i] / 2) + ); + }); + }; + + await selectBasePalette('Grayscale'); + await expectUncolored(); + await selectBasePalette('DICOM Hot Iron'); + await expectColored(); + await setRange('-2000', '-1000'); + await expectRange(-2000, -1000); + await expectUncolored(); + await $('[data-testid="base-image-contrast"]').click(); + await $('.v-menu.v-overlay--active .v-card').waitForStable(); + await $('.v-menu.v-overlay--active').$('label=Full Range').click(); + await browser.keys('Escape'); + await expectRange(-1000, 500); + await expectColored(); + const visibility = $('[data-testid="base-image-visibility"]'); + await visibility.click(); + await expectUncolored(); + await selectLayout('Four Up', 3, true); + await expect(visibility).toHaveText(expect.stringContaining('SHOW')); + await expectRange(-1000, 500); + await expect($('[data-testid="base-image-colormap"]')).toHaveText( + expect.stringContaining('DICOM Hot Iron') + ); + await volViewPage.layoutButton.click(); + await $('.v-menu.v-overlay--active .v-overlay__content').waitForStable(); + await $('.v-list-item-title=Oblique').click(); + await $('.oblique-grid').waitForDisplayed(); + await $('.oblique-grid').waitForStable(); + await expectUncolored(); + await visibility.click(); + await expectColored(); + await setRange('-800', '200'); + await $('[data-testid="control-button-Reset Views"]').click(); + await expectRange(-1000, 500); + await expect($('[data-testid="base-image-colormap"]')).toHaveText( + expect.stringContaining('Grayscale') + ); + await expect(visibility).toHaveText(expect.stringContaining('HIDE')); + await expectUncolored(); + }); + + it('preserves the base window through layouts and a saved session without changing overlay settings', async () => { + await openBase(true); + const overlayMin = $('[data-testid="layer-range-min"] input'); + const overlayMax = $('[data-testid="layer-range-max"] input'); + await expect(overlayMin).toBeEnabled(); + + const overlaySettings = async () => ({ + min: await overlayMin.getAttribute('title'), + max: await overlayMax.getAttribute('title'), + palette: await $('[data-testid="layer-colormap"]').getText(), + opacity: await $('[data-testid="layer-opacity-slider"] input').getValue(), + transparent: await $( + '[data-testid="layer-transparent-background"] input' + ).isSelected(), + }); + + const initialOverlay = await overlaySettings(); + await selectBasePalette('Viridis'); + await setRange('-800', '200'); + await expectRange(-800, 200); + expect(await overlaySettings()).toEqual(initialOverlay); + const visibility = () => $('[data-testid="base-image-visibility"]'); + await expect(visibility()).toHaveText(expect.stringContaining('HIDE')); + expect((await visibility().getLocation()).x).toBeLessThan( + (await $('[data-testid="base-image-colormap"]').getLocation()).x + ); + expect((await visibility().getCSSProperty('border-radius')).value).toBe( + ( + await $('[data-testid="base-image-contrast"]').getCSSProperty( + 'border-radius' + ) + ).value + ); + await $('header').moveTo(); + const shown = await capture(); + await visibility().click(); + await expect(visibility()).toHaveText(expect.stringContaining('SHOW')); + await $('header').moveTo(); + await browser.waitUntil(async () => { + const hidden = await capture(); + return ( + hidden.length === 3 && + hidden.every( + (view, i) => + view.colored > 1000 && view.checksum !== shown[i].checksum + ) + ); + }); + await expectRange(-800, 200); + expect(await overlaySettings()).toEqual(initialOverlay); + await visibility().click(); + await $('header').moveTo(); + await browser.waitUntil(async () => { + const restored = await capture(); + return ( + restored.length === 3 && + restored.every((view, i) => view.checksum === shown[i].checksum) + ); + }); + await visibility().click(); + await selectLayout('3D Only', 0, true); + await expect(field('min')).toBeDisplayed(); + await expect(visibility()).toBeDisabled(); + await expect(field('min')).toBeDisabled(); + await expect(field('max')).toBeDisabled(); + await expect($('[data-testid="base-image-contrast"]')).toBeDisabled(); + await expect($('[data-testid="base-image-colormap"] input')).toBeDisabled(); + for (const endpoint of ['min', 'max', 'center'] as const) + await expect(handle(endpoint)).toHaveAttribute('tabindex', '-1'); + await $('[data-testid="base-image-properties"] .ms-4').moveTo(); + await expect( + $('.v-tooltip.v-overlay--active .v-overlay__content') + ).toHaveText( + 'Open a 2D or oblique view showing this image to adjust window/level.' + ); + await selectLayout('Axial Only', 1, false); + await expect(visibility()).toHaveText(expect.stringContaining('SHOW')); + await expectRange(-800, 200); + await expect(field('min')).toBeEnabled(); + const saved = await volViewPage.saveSession(); + await waitForDownload(join(TEMP_DIR, saved), SESSION_SAVE_TIMEOUT); + await openVolViewPage(saved); + await volViewPage.renderingModuleTab.click(); + await field('min').scrollIntoView({ block: 'center', behavior: 'instant' }); + await expectRange(-800, 200); + await selectLayout('Four Up', 3, true); + await expect(visibility()).toHaveText(expect.stringContaining('SHOW')); + await expectRange(-800, 200); + expect(await overlaySettings()).toEqual(initialOverlay); + await expect($('[data-testid="base-image-colormap"]')).toHaveText( + expect.stringContaining('Viridis') + ); + await $('header').moveTo(); + const hidden = await capture(); + await visibility().click(); + await expect(visibility()).toHaveText(expect.stringContaining('HIDE')); + await $('header').moveTo(); + await expectViewsChanged(hidden); + expect(await overlaySettings()).toEqual(initialOverlay); + }); +}); diff --git a/tests/specs/layer-display-controls.e2e.ts b/tests/specs/layer-display-controls.e2e.ts new file mode 100644 index 000000000..aa0dfa26b --- /dev/null +++ b/tests/specs/layer-display-controls.e2e.ts @@ -0,0 +1,661 @@ +import { + layeredImageManifest, + openLayerDisplayControls, +} from './layerDisplayTestUtils'; +import * as path from 'path'; +import { TEMP_DIR } from '@/wdio.shared.conf'; +import { setValueVueInput, volViewPage } from '../pageobjects/volview.page'; +import { + capture2DViewScreenshotMetrics, + selectLayout, + openVolViewPage, + SESSION_SAVE_TIMEOUT, + waitForDownload, + writeManifestToFile, + writeMetaImage, +} from './utils'; + +const paletteTitles = [ + 'Grayscale', + 'Inverted grayscale', + 'DICOM Hot Iron', + 'DICOM PET', + 'DICOM Hot Metal Blue', + 'DICOM Spring', + 'DICOM Winter', + 'Viridis', + 'Cool to Warm', +]; + +async function expectPaletteOptions() { + const options = $$('[role="option"]'); + const expected = paletteTitles; + await expect(options).toBeElementsArrayOfSize(expected.length); + for (const [index, title] of expected.entries()) { + const option = options[index]; + await option.scrollIntoView({ block: 'nearest', behavior: 'instant' }); + await expect(option).toHaveText(title); + await expect( + option.$('[data-testid="layer-palette-option-swatch"]') + ).toHaveAttribute('style', expect.stringContaining('linear-gradient(')); + } +} + +async function captureViewPixels() { + return capture2DViewScreenshotMetrics('overlay-capture.png'); +} + +async function windowingLabels() { + const views = await volViewPage.getViews2D(); + return views.map(async (view) => { + const text = await view.getText(); + return text.match(/W\/L: [\d.-]+ \/ [\d.-]+/)?.[0]; + }); +} + +async function expectAppliedRange( + min: string, + max: string, + displayedMax = max +) { + await $('h4*=display-overlay.mha').click(); + await expect($('[data-testid="layer-range-min"] input')).toHaveValue(min); + await expect($('[data-testid="layer-range-max"] input')).toHaveValue( + displayedMax + ); + await expect($('[data-testid="layer-range-min"] input')).toHaveAttribute( + 'title', + min + ); + await expect($('[data-testid="layer-range-max"] input')).toHaveAttribute( + 'title', + max + ); +} + +async function expectDisplaySettings( + min: string, + max: string, + displayedMax = max +) { + await expectAppliedRange(min, max, displayedMax); + await expect($('[data-testid="layer-colormap"]')).toHaveText( + expect.stringContaining('Cool to Warm') + ); + await expect( + $('[data-testid="layer-opacity-slider"] [role="slider"]') + ).toHaveAttribute('aria-valuenow', '0.5'); + await expect( + $('[data-testid="layer-transparent-background"] input') + ).not.toBeChecked(); +} + +async function expectRenderedPixels( + expected: Awaited> +) { + await $('header').moveTo(); + await browser.waitUntil( + async () => { + const pixels = await captureViewPixels(); + return ( + pixels.length === expected.length && + pixels.every( + ({ colored, blue, checksum }, index) => + colored === expected[index].colored && + blue === expected[index].blue && + checksum === expected[index].checksum + ) + ); + }, + { + timeoutMsg: + 'Expected matching overlay color counts and intensity sum after replacing views or reopening the session', + } + ); +} + +async function openDisplayFixture({ + preset = 'Cool to Warm', + mappingRange = [-2.75, 100.25], + opacity = 0.5, + transparentBackground = false, +} = {}) { + const display = { preset, mappingRange, opacity, transparentBackground }; + const base = writeMetaImage('display-base.mha'); + const overlay = writeMetaImage('display-overlay.mha', { step: 29 }); + const manifest = 'display-controls.json'; + await writeManifestToFile( + { + ...layeredImageManifest(base, overlay), + layerDisplay: { + 'base::overlay': display, + }, + }, + manifest + ); + + await openVolViewPage(manifest); + const baseCard = $(`.image-list-card*=${base}`); + await baseCard.waitForClickable(); + await baseCard.$('[data-testid="dataset-menu-button"]').click(); + const showInAllViews = $('.v-list-item*=Show in all views'); + await showInAllViews.waitForClickable(); + await showInAllViews.click(); + await browser.keys('Escape'); + await openLayerDisplayControls(); + await $('header').moveTo(); + await browser.waitUntil( + async () => { + const pixels = await captureViewPixels(); + return ( + pixels.length === 3 && pixels.every(({ colored }) => colored > 1000) + ); + }, + { timeoutMsg: 'Expected the overlay to render in all three 2D views' } + ); +} + +async function openEditedDisplayFixture() { + await openDisplayFixture({ + preset: 'DICOM Hot Iron', + mappingRange: [0, 255], + opacity: 0.6, + }); + const select = $('[data-testid="layer-colormap"]'); + await select.scrollIntoView({ block: 'center', behavior: 'instant' }); + await select.click(); + const option = $('[role="option"]*=Cool to Warm'); + await option.scrollIntoView({ block: 'center', behavior: 'instant' }); + await option.waitForClickable(); + await option.click(); + await setValueVueInput($('[data-testid="layer-range-min"] input'), '-2.75'); + await setValueVueInput($('[data-testid="layer-range-max"] input'), '100.25'); + await browser.execute(() => { + document + .querySelector( + '[data-testid="layer-opacity-slider"] [role="slider"]' + )! + .focus(); + }); + await browser.keys('PageDown'); + await expectDisplaySettings('-2.75', '100.25', '100.3'); + await $('header').moveTo(); + await browser.waitUntil( + async () => { + const pixels = await captureViewPixels(); + return ( + pixels.length === 3 && + pixels.every(({ colored, blue }) => colored > 1000 && blue > 0) + ); + }, + { + timeoutMsg: 'Expected the edited Cool to Warm palette in all three views', + } + ); + const baseWindowing = await windowingLabels(); + expect(baseWindowing.every(Boolean)).toBe(true); + const appliedPixels = await captureViewPixels(); + return { baseWindowing, appliedPixels }; +} + +describe('Overlay display controls', () => { + for (const { title, preset } of [ + { title: 'missing', preset: undefined }, + { title: 'unknown', preset: 'Unknown palette' }, + ]) { + it(`uses Hot Iron for a ${title} saved palette`, async () => { + const base = writeMetaImage(`fallback-${title}-base.mha`); + const overlay = writeMetaImage(`fallback-${title}-overlay.mha`, { + step: 29, + }); + const manifest = `fallback-${title}.json`; + await writeManifestToFile( + { + ...layeredImageManifest(base, overlay), + layerDisplay: { + 'base::overlay': { + ...(preset === undefined ? {} : { preset }), + mappingRange: [20, 200], + opacity: 0.4, + transparentBackground: false, + }, + }, + }, + manifest + ); + await openVolViewPage(manifest); + await openLayerDisplayControls(); + const select = $('[data-testid="layer-colormap"]'); + await expect(select).toHaveText( + expect.stringContaining('DICOM Hot Iron') + ); + await expect($('[data-testid="layer-range-min"] input')).toHaveValue( + '20' + ); + await expect($('[data-testid="layer-range-max"] input')).toHaveValue( + '200' + ); + await expect( + $('[data-testid="layer-opacity-slider"] [role="slider"]') + ).toHaveAttribute('aria-valuenow', '0.4'); + await expect( + $('[data-testid="layer-transparent-background"] input') + ).not.toBeChecked(); + await select.scrollIntoView({ block: 'center', behavior: 'instant' }); + await select.click(); + await expect($('[role="option"]*=DICOM Hot Iron')).toHaveAttribute( + 'aria-selected', + 'true' + ); + await browser.keys('Escape'); + }); + } + + it('applies palette, opacity and range edits in every view and preserves pixels for invalid drafts', async () => { + await openDisplayFixture({ + preset: 'DICOM Hot Iron', + mappingRange: [0, 255], + opacity: 1, + transparentBackground: true, + }); + const transparent = $('[data-testid="layer-transparent-background"] input'); + await expect(transparent).toBeChecked(); + await browser.waitUntil( + async () => (await $$('.Vue-Toastification__toast').length) === 0 + ); + await transparent.scrollIntoView({ block: 'center', behavior: 'instant' }); + await transparent.click(); + await expect(transparent).not.toBeChecked(); + const select = $('[data-testid="layer-colormap"]'); + await expect(select).toHaveText(expect.stringContaining('DICOM Hot Iron')); + const rangeHandles = await $$( + '[data-testid="layer-range-slider"] [role="slider"]' + ); + expect( + await rangeHandles.map((handle) => handle.getComputedLabel()) + ).toEqual( + expect.arrayContaining([ + 'Display range minimum', + 'Display range maximum', + 'Display range center', + ]) + ); + expect(rangeHandles).toHaveLength(3); + await expect( + await $( + '[data-testid="layer-opacity-slider"] [role="slider"]' + ).getComputedLabel() + ).toBe('Opacity'); + await browser.waitUntil( + async () => (await $$('.Vue-Toastification__toast').length) === 0 + ); + await $('[data-testid="layer-range-min"] input').scrollIntoView({ + block: 'center', + behavior: 'instant', + }); + await select.waitForClickable(); + await browser.execute(() => { + document + .querySelector( + '[data-testid="layer-opacity-slider"] [role="slider"]' + )! + .focus(); + }); + await browser.keys('Home'); + for (let step = 0; step < 5; step++) await browser.keys('PageUp'); + await expect( + $('[data-testid="layer-opacity-slider"] [role="slider"]') + ).toHaveAttribute('aria-valuenow', '0.5'); + + const views = await volViewPage.getViews2D(); + expect(await views.length).toBe(3); + const baseWindowing = await windowingLabels(); + expect(baseWindowing.every(Boolean)).toBe(true); + await select.scrollIntoView({ block: 'center', behavior: 'instant' }); + await browser.waitUntil( + async () => (await $$('.Vue-Toastification__toast').length) === 0 + ); + await select.click(); + await expect($('[role="option"]*=DICOM Hot Iron')).toHaveAttribute( + 'aria-selected', + 'true' + ); + const coolToWarm = $('[role="option"]*=Cool to Warm'); + await coolToWarm.scrollIntoView({ block: 'center', behavior: 'instant' }); + await coolToWarm.waitForClickable(); + await coolToWarm + .$('[data-testid="layer-palette-option-swatch"]') + .waitForDisplayed(); + await browser.saveScreenshot( + path.join(TEMP_DIR, 'overlay-palette-menu.png') + ); + await coolToWarm.click(); + await expect(select).toHaveText(expect.stringContaining('Cool to Warm')); + await browser.waitUntil( + async () => { + const pixels = await captureViewPixels(); + return pixels.length === 3 && pixels.every(({ blue }) => blue > 1000); + }, + { + timeoutMsg: 'Expected palette to change pixels in all three 2D views', + } + ); + + const beforeRange = await captureViewPixels(); + await $('[data-testid="layer-range-min"] input').scrollIntoView({ + block: 'center', + behavior: 'instant', + }); + await $('[data-testid="layer-range-min"] input').waitForClickable(); + await setValueVueInput($('[data-testid="layer-range-min"] input'), '-2.75'); + await setValueVueInput( + $('[data-testid="layer-range-max"] input'), + '100.25' + ); + await expectAppliedRange('-2.75', '100.25', '100.3'); + await $('[data-testid="layer-range-max"] input').click(); + await expect($('[data-testid="layer-range-max"] input')).toHaveValue( + '100.3' + ); + await expect($('[data-testid="layer-range-max"] input')).toHaveAttribute( + 'title', + '100.25' + ); + await expectAppliedRange('-2.75', '100.25', '100.3'); + await browser.waitUntil( + async () => { + const after = await captureViewPixels(); + return ( + after.length === 3 && + after.every( + ({ colored, blue, checksum }, index) => + colored > 1000 && + blue < beforeRange[index].blue && + checksum !== beforeRange[index].checksum + ) + ); + }, + { + timeoutMsg: 'Expected range to change pixels in all three 2D views', + } + ); + await expect($('[data-testid="layer-opacity-slider"] input')).toHaveValue( + '0.5' + ); + await expect( + $('[data-testid="layer-opacity-slider"] [role="slider"]') + ).toHaveAttribute('aria-valuenow', '0.5'); + await expectAppliedRange('-2.75', '100.25', '100.3'); + expect(await windowingLabels()).toEqual(baseWindowing); + await $('header').moveTo(); + const appliedPixels = await captureViewPixels(); + await setValueVueInput($('[data-testid="layer-range-max"] input'), '-3'); + await $('[data-testid="layer-range-max"] input').click(); + await browser.keys('Enter'); + await expect($('[data-testid="layer-range-status"]')).toHaveText( + 'Min must be less than Max.' + ); + await expect($('[data-testid="layer-range-min"] input')).toHaveAttribute( + 'title', + '-2.75' + ); + await expect($('[data-testid="layer-range-max"] input')).toHaveAttribute( + 'title', + '100.25' + ); + expect(await captureViewPixels()).toEqual(appliedPixels); + + await setValueVueInput( + $('[data-testid="layer-range-max"] input'), + '100.25' + ); + await setValueVueInput($('[data-testid="layer-range-min"] input'), '0'); + await setValueVueInput($('[data-testid="layer-range-max"] input'), '255'); + await $('header').moveTo(); + await expectAppliedRange('0', '255'); + await expect($('[data-testid="layer-colormap"]')).toHaveText( + expect.stringContaining('Cool to Warm') + ); + await expect( + $('[data-testid="layer-opacity-slider"] [role="slider"]') + ).toHaveAttribute('aria-valuenow', '0.5'); + await browser.waitUntil( + async () => { + const fullRange = await captureViewPixels(); + return ( + fullRange.length === 3 && + fullRange.every(({ blue }, index) => blue === beforeRange[index].blue) + ); + }, + { + timeoutMsg: + 'Expected entering the full image range to restore the colors in all three views', + } + ); + await browser.execute(() => { + document + .querySelector('[data-testid="layer-range-max-handle"]')! + .focus(); + }); + for (let step = 0; step < 10; step++) await browser.keys('ArrowDown'); + await expect($('[data-testid="layer-range-min"] input')).toHaveValue('0'); + await expect($('[data-testid="layer-range-max"] input')).toHaveValue( + '229.5' + ); + expect( + Number( + await $('[data-testid="layer-range-max"] input').getAttribute('title') + ) + ).toBeCloseTo(229.5, 8); + await $('header').moveTo(); + await browser.waitUntil( + async () => { + const pixels = await captureViewPixels(); + return ( + pixels.length === 3 && + pixels.every(({ blue }, index) => blue < beforeRange[index].blue) + ); + }, + { + timeoutMsg: + 'Expected the range slider to update the overlay immediately in all three views', + } + ); + await browser.saveScreenshot( + path.join(TEMP_DIR, 'overlay-display-controls.png') + ); + }); + + it('edits overlay opacity in 3D Only and applies it when returning to slices', async () => { + const { baseWindowing, appliedPixels } = await openEditedDisplayFixture(); + await selectLayout('3D Only', 0, true); + for (const name of [ + 'colormap', + 'range-min', + 'range-max', + 'transparent-background', + 'opacity-slider', + ]) { + await expect($(`[data-testid="layer-${name}"]`)).toBeDisplayed(); + await expect($(`[data-testid="layer-${name}"] input`)).toBeEnabled(); + } + for (const handle of await $$( + '[data-testid="layer-range-slider"] [role="slider"]' + )) + await expect(handle).toHaveAttribute('tabindex', '0'); + const volumeSettings = await $$('.mapping-range-editor input').map( + (input) => input.getValue() + ); + expect(volumeSettings).toHaveLength(2); + const opacity = $('[data-testid="layer-opacity-slider"] [role="slider"]'); + await opacity.execute((element) => element.focus()); + await browser.keys('Home'); + await expect(opacity).toHaveAttribute('aria-valuenow', '0'); + expect( + await $$('.mapping-range-editor input').map((input) => input.getValue()) + ).toEqual(volumeSettings); + + await selectLayout('Axial Only', 1, false); + await expectAppliedRange('-2.75', '100.25', '100.3'); + await expect(opacity).toHaveAttribute('aria-valuenow', '0'); + await $('header').moveTo(); + await browser.waitUntil( + async () => { + const pixels = await captureViewPixels(); + return pixels.length === 1 && pixels[0].colored < 100; + }, + { + timeoutMsg: + 'Expected the replacement axial view to show the base without colored overlay pixels', + } + ); + await selectLayout('3D Only', 0, true); + await opacity.execute((element) => element.focus()); + await browser.keys(Array.from({ length: 50 }, () => 'ArrowRight')); + await expectDisplaySettings('-2.75', '100.25', '100.3'); + expect( + await $$('.mapping-range-editor input').map((input) => input.getValue()) + ).toEqual(volumeSettings); + 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 after converting every 2D view and saving a 3D-only session', 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); + 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 expectDisplaySettings('-2.75', '100.25', '100.3'); + expect(await windowingLabels()).toEqual(baseWindowing); + await expectRenderedPixels(appliedPixels); + }); + + it('preserves transparent background and rendered colors when reopening a session', async () => { + await openDisplayFixture({ mappingRange: [0, 229.5] }); + await expectDisplaySettings('0', '229.5'); + const beforeTransparent = await captureViewPixels(); + const restoredTransparent = $( + '[data-testid="layer-transparent-background"] input' + ); + await restoredTransparent.scrollIntoView({ + block: 'center', + behavior: 'instant', + }); + await restoredTransparent.click(); + await expect(restoredTransparent).toBeChecked(); + await $('header').moveTo(); + let transparentPixels = await captureViewPixels(); + await browser.waitUntil( + async () => { + transparentPixels = await captureViewPixels(); + return ( + transparentPixels.length === 3 && + transparentPixels.every( + ({ checksum }, index) => + checksum !== beforeTransparent[index].checksum + ) + ); + }, + { + timeoutMsg: + 'Expected transparent background to update every view before saving', + } + ); + const transparentSession = await volViewPage.saveSession(); + await waitForDownload( + path.join(TEMP_DIR, transparentSession), + SESSION_SAVE_TIMEOUT + ); + await openVolViewPage(transparentSession); + await openLayerDisplayControls(); + await expect( + $('[data-testid="layer-transparent-background"] input') + ).toBeChecked(); + await expectRenderedPixels(transparentPixels); + + const select = $('[data-testid="layer-colormap"]'); + await select.scrollIntoView({ block: 'center', behavior: 'instant' }); + await select.click(); + await expect($('[role="option"]*=Cool to Warm')).toHaveAttribute( + 'aria-selected', + 'true' + ); + await browser.keys('Escape'); + await browser.saveScreenshot( + path.join(TEMP_DIR, 'overlay-transparent-restored.png') + ); + }); + + it('offers the curated palette inventory and renders Spring and Winter in every view', async () => { + await openDisplayFixture({ + mappingRange: [0, 229.5], + transparentBackground: true, + }); + const select = $('[data-testid="layer-colormap"]'); + await select.scrollIntoView({ + block: 'center', + behavior: 'instant', + }); + await select.click(); + await expectPaletteOptions(); + await expect($('[role="option"]*=Cool to Warm')).toHaveAttribute( + 'aria-selected', + 'true' + ); + for (const title of ['DICOM Spring', 'DICOM Winter']) { + await browser.keys('Escape'); + await $('header').moveTo(); + const beforePalette = await captureViewPixels(); + await select.click(); + const option = $(`[role="option"]*=${title}`); + await option.scrollIntoView({ block: 'center', behavior: 'instant' }); + await option.waitForClickable(); + await expect( + option.$('[data-testid="layer-palette-option-swatch"]') + ).toBeDisplayed(); + await option.click(); + await expect(select).toHaveText(expect.stringContaining(title)); + await $('header').moveTo(); + await browser.waitUntil( + async () => { + const pixels = await captureViewPixels(); + return ( + pixels.length === 3 && + pixels.every( + ({ checksum }, index) => + checksum !== beforePalette[index].checksum + ) + ); + }, + { + timeoutMsg: `Expected ${title} to change colors in all three 2D views`, + } + ); + await select.click(); + await expect($(`[role="option"]*=${title}`)).toHaveAttribute( + 'aria-selected', + 'true' + ); + } + await browser.saveScreenshot( + path.join(TEMP_DIR, 'overlay-short-palette-menu.png') + ); + await browser.keys('Escape'); + expect(await volViewPage.getNotificationsCount()).toEqual(0); + }); +}); diff --git a/tests/specs/layerDisplayTestUtils.ts b/tests/specs/layerDisplayTestUtils.ts new file mode 100644 index 000000000..2b3d8ff46 --- /dev/null +++ b/tests/specs/layerDisplayTestUtils.ts @@ -0,0 +1,33 @@ +import { volViewPage } from '../pageobjects/volview.page'; +import { waitForModuleTransitions } from './utils'; + +export const layeredImageManifest = (base: string, overlay: string) => ({ + version: '7.0.0', + dataSources: [base, overlay].map((name, index) => ({ + id: index + 1, + type: 'uri', + uri: `/tmp/${name}`, + name: name.split('/').at(-1), + })), + datasets: [ + { id: 'base', dataSourceId: 1 }, + { id: 'overlay', dataSourceId: 2 }, + ], + primarySelection: 'base', + parentToLayers: [{ selectionKey: 'base', sourceSelectionKeys: ['overlay'] }], +}); + +export async function openLayerDisplayControls() { + await volViewPage.renderingModuleTab.click(); + const volume = $('[data-testid="volume-rendering-toggle"]'); + if ( + (await volume.isExisting()) && + (await volume.getAttribute('aria-expanded')) === 'true' + ) + await volume.click(); + await waitForModuleTransitions(); + await $('[data-testid="layer-colormap"]').waitForDisplayed(); + await browser.waitUntil( + async () => (await $$('.Vue-Toastification__toast').length) === 0 + ); +} diff --git a/tests/specs/layers-and-rendering.e2e.ts b/tests/specs/layers-and-rendering.e2e.ts index 50d53a4f8..fba5789e9 100644 --- a/tests/specs/layers-and-rendering.e2e.ts +++ b/tests/specs/layers-and-rendering.e2e.ts @@ -2,8 +2,14 @@ import * as fs from 'fs'; import * as path from 'path'; import { cleanuptotal } from 'wdio-cleanuptotal-service'; import { DOWNLOAD_TIMEOUT, FIXTURES, TEMP_DIR } from '@/wdio.shared.conf'; -import { volViewPage } from '../pageobjects/volview.page'; -import { openUrls, openVolViewPage, writeManifestToFile } from './utils'; +import { setValueVueInput, volViewPage } from '../pageobjects/volview.page'; +import { + capture2DViewScreenshotMetrics, + openUrls, + openVolViewPage, + waitForModuleTransitions, + writeManifestToFile, +} from './utils'; import { PROSTATEX_DATASET } from '../datasets'; const openLayeringFixtures = async () => { @@ -55,6 +61,57 @@ describe('Layers and Rendering', () => { await expect(volumeRenderingSection).toBeDisplayed(); await expect(pwfCanvas).toExist(); + + const volumePanel = $('[data-testid="volume-rendering-panel"]'); + const toggle = $('[data-testid="volume-rendering-toggle"]'); + const presets = volumePanel.$('.v-expansion-panel-title*=Color Presets'); + const cinematic = volumePanel.$( + '.v-expansion-panel-title*=Cinematic Rendering' + ); + const shift = volumePanel + .$('.mapping-range-editor .v-slider') + .$('[role="slider"]'); + await expect(toggle).toHaveAttribute('aria-expanded', 'true'); + await expect(presets).toBeDisplayed(); + await expect(cinematic).toBeDisplayed(); + const initialShift = await shift.getAttribute('aria-valuenow'); + await shift.execute((element) => element.focus()); + await browser.keys('ArrowRight'); + await expect(shift).not.toHaveAttribute( + 'aria-valuenow', + initialShift ?? '' + ); + const editedShift = await shift.getAttribute('aria-valuenow'); + if (editedShift === null) throw new Error('Expected a displayed 3D shift'); + + await toggle.scrollIntoView({ block: 'center', behavior: 'instant' }); + await toggle.click(); + await expect(toggle).toHaveAttribute('aria-expanded', 'false'); + await waitForModuleTransitions(); + await expect(pwfCanvas).not.toBeDisplayed(); + await expect(presets).not.toBeDisplayed(); + await expect(cinematic).not.toBeDisplayed(); + const minimum = $('[data-testid="base-image-range-min"] input'); + const maximum = $('[data-testid="base-image-range-max"] input'); + await expect(minimum).toBeDisplayed(); + await expect(minimum).toBeEnabled(); + await setValueVueInput(minimum, '0'); + await setValueVueInput(maximum, '100'); + await browser.keys('Enter'); + for (const view of await volViewPage.getViews2D()) + await expect(view).toHaveText( + expect.stringContaining('W/L: 100.00 / 50.00') + ); + + await toggle.click(); + await expect(toggle).toHaveAttribute('aria-expanded', 'true'); + await waitForModuleTransitions(); + await expect(pwfCanvas).toBeDisplayed(); + await expect(presets).toBeDisplayed(); + await expect(cinematic).toBeDisplayed(); + await expect(shift).toHaveAttribute('aria-valuenow', editedShift); + await expect(minimum).toHaveAttribute('title', '0'); + await expect(maximum).toHaveAttribute('title', '100'); }); it('should create overlay with 2 DICOM images', async () => { @@ -115,27 +172,9 @@ describe('Layers and Rendering', () => { ); await addLayerButton.click(); - await browser.waitUntil( - () => - browser.execute(() => { - const app = (document.querySelector('#app') as any)?.__vue_app__; - const pinia = app?.config?.globalProperties?.$pinia; - const imageCache = pinia?._s?.get('image-cache'); - const layerId = imageCache?.imageIds?.find((id: string) => - id.includes('::') - ); - return layerId && imageCache.imageStatus[layerId] === 'complete'; - }), - { - timeout: DOWNLOAD_TIMEOUT, - timeoutMsg: 'Expected the layer image to finish loading', - } - ); - const renderTab = await volViewPage.renderingModuleTab; await renderTab.click(); - // need to wait a little for layer section to render await browser.waitUntil( async function slidersExist() { const layerOpacitySliders = await volViewPage.layerOpacitySliders; @@ -146,59 +185,51 @@ describe('Layers and Rendering', () => { } ); + const minimum = $('[data-testid="layer-range-min"] input'); + const maximum = $('[data-testid="layer-range-max"] input'); + await minimum.waitForEnabled(); + await waitForModuleTransitions(); + // The resampled fixture spans scalar values 0 through 763. + await setValueVueInput(minimum, '0'); + await setValueVueInput(maximum, '763'); + await expect(minimum).toHaveAttribute('title', '0'); + await expect(maximum).toHaveAttribute('title', '763'); + const layerOpacitySliders = await volViewPage.layerOpacitySliders; const firstSlider = layerOpacitySliders[0]; const sliderThumb = await firstSlider.$('[role="slider"]'); const inputElement = await firstSlider.$('input'); - const views2D = await volViewPage.getViews2D(); - const viewCount = await views2D.length; - if (!views2D || viewCount === 0) { - throw new Error('Could not find 2D views for screenshot'); - } - const firstView2D = views2D[0]; - const beforeOpacityChange = await firstView2D.takeScreenshot(); - - await browser.execute((element) => { - (element as HTMLElement).focus(); - }, sliderThumb); - - await browser.keys('End'); - + await sliderThumb.execute((element) => element.focus()); + await browser.keys('Home'); + await expect(inputElement).toHaveValue('0'); + await $('header').moveTo(); await browser.waitUntil( async () => { - const value = await inputElement.getValue(); - return value === '1'; + const [axial] = await capture2DViewScreenshotMetrics( + 'layers-zero-opacity.png' + ); + return axial?.colored === 0 && axial.checksum > 1000; }, { - timeoutMsg: 'Expected slider value to be 1', - timeout: 5000, + timeoutMsg: 'Expected grayscale MRA anatomy at zero overlay opacity', } ); + await sliderThumb.execute((element) => element.focus()); + await browser.keys('End'); + await expect(inputElement).toHaveValue('1'); + await $('header').moveTo(); await browser.waitUntil( - async () => (await firstView2D.takeScreenshot()) !== beforeOpacityChange, - { - timeout: 5000, - timeoutMsg: 'Expected the 2D view to render the opacity change', - } - ); - - const result = await browser.checkElement( - firstView2D, - 'layers_max_opacity_2d_view', + async () => { + const [axial] = await capture2DViewScreenshotMetrics( + 'layers-full-opacity.png' + ); + return axial?.colored > 1000; + }, { - pixelmatch: { threshold: 0.2 }, - // Compare where the resampled prostate layer intersects the MRA. - blockOut: [ - { x: 0, y: 0, width: 371, height: 130 }, - { x: 0, y: 240, width: 371, height: 92 }, - { x: 0, y: 130, width: 20, height: 110 }, - { x: 340, y: 130, width: 31, height: 110 }, - ], + timeoutMsg: + 'Expected the resampled prostate overlay to add colored pixels to the MRA', } ); - - const THRESHOLD = 1; // percent - await expect(result).toBeLessThan(THRESHOLD); }); }); diff --git a/tests/specs/legacy-window-appearance.e2e.ts b/tests/specs/legacy-window-appearance.e2e.ts new file mode 100644 index 000000000..81a0a875d --- /dev/null +++ b/tests/specs/legacy-window-appearance.e2e.ts @@ -0,0 +1,134 @@ +import { readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import JSZip from 'jszip'; +import type { z } from 'zod'; +import type { ManifestSchema } from '@/src/io/state-file/schema'; +import { TEMP_DIR } from '../../wdio.shared.conf'; +import { ONE_CT_SLICE_DICOM } from '../datasets'; +import { setValueVueInput, volViewPage } from '../pageobjects/volview.page'; +import { + capture2DViewScreenshotMetrics, + openThroughDialog, + openUrls, + selectLayout, + SESSION_SAVE_TIMEOUT, + waitForDownload, + waitForModuleTransitions, +} from './utils'; + +const field = (endpoint: 'min' | 'max') => + $(`[data-testid="base-image-range-${endpoint}"] input`); +const visibility = () => $('[data-testid="base-image-visibility"]'); +const palette = () => $('[data-testid="base-image-colormap"]'); +const capture = () => + capture2DViewScreenshotMetrics('legacy-window-appearance.png'); + +const expectRange = async (min: number, max: number) => { + await expect(field('min')).toHaveAttribute('title', String(min)); + await expect(field('max')).toHaveAttribute('title', String(max)); + await expect((await volViewPage.getViews2D())[0]).toHaveText( + expect.stringContaining( + `W/L: ${(max - min).toFixed(2)} / ${((max + min) / 2).toFixed(2)}` + ) + ); +}; + +const setRange = async (min: number, max: number) => { + await setValueVueInput(field('min'), String(min)); + await setValueVueInput(field('max'), String(max)); + await browser.keys('Enter'); + await expectRange(min, max); +}; + +const saveSession = async () => { + const filename = await volViewPage.saveSession(); + const filePath = join(TEMP_DIR, filename); + await waitForDownload(filePath, SESSION_SAVE_TIMEOUT); + return filePath; +}; + +const omitSavedAppearance = async (filePath: string) => { + const zip = await JSZip.loadAsync(readFileSync(filePath)); + const manifest = JSON.parse( + await zip.file('manifest.json')!.async('string') + ) as z.input; + const windows = Object.values(manifest.viewByID ?? {}).flatMap((view) => + Object.values(view.config ?? {}).flatMap(({ window }) => window ?? []) + ); + expect(windows.length).toBeGreaterThan(0); + windows.forEach((window) => { + delete window.visibility; + delete window.preset; + }); + zip.file('manifest.json', JSON.stringify(manifest)); + const legacyPath = join(TEMP_DIR, 'legacy-window.volview.zip'); + writeFileSync(legacyPath, await zip.generateAsync({ type: 'nodebuffer' })); + return legacyPath; +}; + +const clearTransientUI = async () => { + await $('header').moveTo(); + await browser.waitUntil( + async () => (await $$('.Vue-Toastification__toast').length) === 0 + ); +}; + +const restoreLegacySession = async (filePath: string) => { + await openThroughDialog(filePath); + await expectRange(-100, 200); + await expect(palette()).toHaveText(expect.stringContaining('Grayscale')); + await expect(visibility()).toHaveText(expect.stringContaining('HIDE')); + await clearTransientUI(); +}; + +describe('Legacy saved base appearance', () => { + it('restores grayscale and visibility when reopening the same DICOM session', async () => { + await openUrls([ONE_CT_SLICE_DICOM]); + await selectLayout('Axial Only', 1, false); + await volViewPage.renderingModuleTab.click(); + await waitForModuleTransitions(); + await field('min').scrollIntoView({ block: 'center', behavior: 'instant' }); + await setRange(-100, 200); + const legacyPath = await omitSavedAppearance(await saveSession()); + await setRange(-120, 180); + await restoreLegacySession(legacyPath); + await browser.waitUntil(async () => { + const [pixels] = await capture(); + return pixels.colored === 0 && pixels.checksum > 1_000_000; + }); + const [original] = await capture(); + + await palette().scrollIntoView({ block: 'center', behavior: 'instant' }); + await palette().click(); + const option = $('[role="option"]*=Viridis'); + await option.scrollIntoView({ block: 'nearest', behavior: 'instant' }); + await option.waitForStable(); + await option.click(); + await setRange(-50, 150); + await clearTransientUI(); + await browser.waitUntil(async () => (await capture())[0].colored > 1000); + await visibility().click(); + await expect(visibility()).toHaveText(expect.stringContaining('SHOW')); + await browser.waitUntil( + async () => (await capture())[0].checksum < original.checksum / 4 + ); + const editedPath = await saveSession(); + + await restoreLegacySession(legacyPath); + await browser.waitUntil( + async () => (await capture())[0].checksum === original.checksum, + { timeoutMsg: 'Expected the original visible grayscale DICOM slice' } + ); + + await openThroughDialog(editedPath); + await expectRange(-50, 150); + await expect(palette()).toHaveText(expect.stringContaining('Viridis')); + await expect(visibility()).toHaveText(expect.stringContaining('SHOW')); + await clearTransientUI(); + await browser.waitUntil( + async () => (await capture())[0].checksum < original.checksum / 4 + ); + await visibility().click(); + await browser.waitUntil(async () => (await capture())[0].colored > 1000); + }); +}); diff --git a/tests/specs/multicomponent-layer-display.e2e.ts b/tests/specs/multicomponent-layer-display.e2e.ts new file mode 100644 index 000000000..5850d1291 --- /dev/null +++ b/tests/specs/multicomponent-layer-display.e2e.ts @@ -0,0 +1,426 @@ +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { + layeredImageManifest, + openLayerDisplayControls, +} from './layerDisplayTestUtils'; +import { TEMP_DIR } from '@/wdio.shared.conf'; +import { setValueVueInput, volViewPage } from '../pageobjects/volview.page'; +import { + captureViewPixels, + makeTempDir, + openVolViewPage, + selectLayout, + SESSION_SAVE_TIMEOUT, + waitForDownload, + waitForModuleTransitions, + writeManifestToFile, +} from './utils'; + +function writeColorImage( + name: string, + channels: number[], + type = 'unsigned char', + alphaBands = false +) { + const directory = makeTempDir(name); + const header = [ + 'NRRD0005', + `type: ${type}`, + 'dimension: 4', + `sizes: ${channels.length} 16 16 16`, + 'kinds: vector domain domain domain', + 'space: left-posterior-superior', + 'space directions: none (1,0,0) (0,1,0) (0,0,1)', + 'space origin: (0,0,0)', + 'encoding: ascii', + ].join('\n'); + const maximum = + type === 'float' ? 1 : type === 'unsigned short' ? 65535 : 255; + const values = Array.from({ length: 4096 }, (_, index) => { + if (channels.length <= 2 && (index === 0 || index === 4095)) + return [index === 0 ? 0 : 255, ...channels.slice(1).map(() => 0)].join( + ' ' + ); + if (alphaBands) { + const x = index % 16; + const y = Math.floor(index / 16) % 16; + const z = Math.floor(index / 256); + const band = (Number(x >= 10) + Number(y >= 10) + Number(z >= 10)) % 3; + return [...channels.slice(0, 3), [0, channels[3], maximum][band]].join( + ' ' + ); + } + return channels.join(' '); + }).join(' '); + writeFileSync(join(directory, `${name}.nrrd`), `${header}\n\n${values}`); + return `${name}/${name}.nrrd`; +} + +async function openOverlayControls( + name: string, + channels: number[], + { savedDisplay = true, type = 'unsigned char', alphaBands = false } = {} +) { + const base = writeColorImage('multicomponent-base', [128]); + const overlay = writeColorImage(name, channels, type, alphaBands); + await writeManifestToFile( + { + ...layeredImageManifest(base, overlay), + ...(savedDisplay + ? { + layerDisplay: { + 'base::overlay': { + preset: 'Grayscale', + mappingRange: [0, 255], + opacity: 1, + transparentBackground: false, + }, + }, + } + : {}), + }, + 'multicomponent-overlay.json' + ); + await openVolViewPage('multicomponent-overlay.json'); + await $(`.image-list-card*=${base.split('/').at(-1)}`) + .$('[data-testid="dataset-menu-button"]') + .click(); + await $('.v-menu.v-overlay--active .v-overlay__content').waitForStable(); + await $('.v-list-item*=Show in all views').click(); + await browser.keys('Escape'); + await openLayerDisplayControls(); + await setValueVueInput($('[data-testid="base-image-range-min"] input'), '0'); + await setValueVueInput( + $('[data-testid="base-image-range-max"] input'), + '255' + ); +} + +async function expectImagePixels( + expectedColors: number[][], + viewCount = 3, + selector = '[data-testid~="vtk-two-view"]', + absentColors: number[][] = [] +) { + await $('header').moveTo(); + await browser.waitUntil( + async () => { + const views = await captureViewPixels( + selector, + 40, + 'multicomponent-layer-display.png' + ); + return ( + views.length === viewCount && + views.every(({ data }) => { + const matchingPixels = (expected: number[]) => { + let matching = 0; + for (let i = 0; i < data.length; i += 4) { + if ( + expected.every( + (value, channel) => Math.abs(data[i + channel] - value) <= 2 + ) + ) + matching++; + } + return matching; + }; + return ( + expectedColors.every( + (expected) => matchingPixels(expected) > 1000 + ) && + absentColors.every((expected) => matchingPixels(expected) === 0) + ); + }) + ); + }, + { + timeoutMsg: `Expected substantial image pixels with RGB ${JSON.stringify(expectedColors)} in ${viewCount} slice views`, + } + ); +} + +const nativeTypes = [ + { type: 'unsigned char', scale: 1 }, + { type: 'unsigned short', scale: 257 }, + { type: 'float', scale: 1 / 255 }, +]; + +const nativeColor = [180, 30, 60]; +const obliqueSlices = + '.oblique-grid > .d-flex:not(:nth-child(2)) [data-testid="vtk-view"]'; + +const blendedColor = (alpha: number, background = 128) => + nativeColor.map((channel) => + Math.round(channel * alpha + background * (1 - alpha)) + ); + +async function showOblique() { + await volViewPage.layoutButton.click(); + await $('.v-menu.v-overlay--active .v-overlay__content').waitForStable(); + await $('.v-list-item-title=Oblique').click(); + await $('.oblique-grid').waitForDisplayed(); +} + +async function openBaseControls() { + await volViewPage.renderingModuleTab.click(); + const volume = $('[data-testid="volume-rendering-toggle"]'); + if ( + (await volume.isExisting()) && + (await volume.getAttribute('aria-expanded')) === 'true' + ) + await volume.click(); + await waitForModuleTransitions(); +} + +describe('Multicomponent image display controls', () => { + for (const { type, scale } of nativeTypes) { + for (const components of [3, 4]) { + it(`renders native ${type} ${components}-component overlays with independent layer opacity`, async () => { + const rgba = components === 4; + const channels = [...nativeColor, ...(rgba ? [128] : [])].map( + (channel) => channel * scale + ); + await openOverlayControls( + `overlay-${type.replace(' ', '-')}-${components}-component`, + channels, + { type, alphaBands: rgba } + ); + for (const name of [ + 'colormap', + 'range-min', + 'range-max', + 'transparent-background', + ]) { + await expect($(`[data-testid="layer-${name}"]`)).toBeDisplayed(); + await expect($(`[data-testid="layer-${name}"] input`)).toBeDisabled(); + } + await expect($('[data-testid="layer-contrast"]')).toBeDisabled(); + await expect( + $('[data-testid="layer-opacity-slider"] input') + ).toBeEnabled(); + const visibility = $('[data-testid="layer-visibility"]'); + await expect(visibility).toBeEnabled(); + + const colors = (opacity: number) => + (rgba ? [0, 128 / 255, 1] : [1]).map((alpha) => + blendedColor(alpha * opacity) + ); + + const foregroundColors = colors(0.5).filter((color) => + color.some((channel) => channel !== 128) + ); + await expectImagePixels(colors(1)); + if (!rgba && type === 'unsigned char') { + const palette = $( + '[data-testid="layer-display-controls"] .colormap-control' + ); + await palette.scrollIntoView({ + block: 'center', + behavior: 'instant', + }); + await palette.moveTo(); + const tooltipId = await palette.getAttribute('aria-describedby'); + await expect( + $(`#${tooltipId}.v-overlay--active .v-overlay__content`) + ).toHaveText( + 'Color map, range, contrast, and background transparency require a single-component overlay.' + ); + } + + const opacity = $( + '[data-testid="layer-opacity-slider"] [role="slider"]' + ); + await opacity.execute((element) => element.focus()); + await browser.keys(Array.from({ length: 50 }, () => 'ArrowLeft')); + await expect(opacity).toHaveAttribute('aria-valuenow', '0.5'); + await expectImagePixels(colors(0.5)); + await browser.keys(Array.from({ length: 50 }, () => 'ArrowLeft')); + await expect(opacity).toHaveAttribute('aria-valuenow', '0'); + await expectImagePixels( + [[128, 128, 128]], + 3, + '[data-testid~="vtk-two-view"]', + foregroundColors + ); + await browser.keys(Array.from({ length: 50 }, () => 'ArrowRight')); + await expect(opacity).toHaveAttribute('aria-valuenow', '0.5'); + await expectImagePixels(colors(0.5)); + + await visibility.click(); + await expect(visibility).toHaveText(expect.stringContaining('SHOW')); + await expectImagePixels( + [[128, 128, 128]], + 3, + '[data-testid~="vtk-two-view"]', + foregroundColors + ); + await visibility.click(); + await expect(visibility).toHaveText(expect.stringContaining('HIDE')); + await expectImagePixels(colors(0.5)); + + if (rgba && type === 'unsigned char') { + await selectLayout('Axial Only', 1, false); + await expectImagePixels(colors(0.5), 1); + const saved = await volViewPage.saveSession(); + await waitForDownload(join(TEMP_DIR, saved), SESSION_SAVE_TIMEOUT); + await openVolViewPage(saved); + await openLayerDisplayControls(); + await expect(opacity).toHaveAttribute('aria-valuenow', '0.5'); + await expectImagePixels(colors(0.5), 1); + await selectLayout('Four Up', 3, true); + await expectImagePixels(colors(0.5)); + } + }); + } + + it(`preserves native ${type} RGB base colors and visibility across axial and oblique views`, async () => { + await openVolViewPage( + writeColorImage( + `base-${type.replace(' ', '-')}-rgb`, + nativeColor.map((channel) => channel * scale), + type + ) + ); + await selectLayout('Axial Only', 1, false); + await openBaseControls(); + for (const name of ['colormap', 'range-min', 'range-max']) + await expect( + $(`[data-testid="base-image-${name}"] input`) + ).toBeDisabled(); + await expect($('[data-testid="base-image-contrast"]')).toBeDisabled(); + await expect( + $('button[data-testid^="control-button-Window & Level"]') + ).toBeDisabled(); + await expectImagePixels([nativeColor], 1); + await showOblique(); + await expectImagePixels([nativeColor], 3, obliqueSlices); + const visibility = $('[data-testid="base-image-visibility"]'); + await expect(visibility).toBeEnabled(); + await visibility.click(); + await expect(visibility).toHaveText(expect.stringContaining('SHOW')); + await expectImagePixels([[0, 0, 0]], 3, obliqueSlices, [nativeColor]); + await visibility.click(); + await expectImagePixels([nativeColor], 3, obliqueSlices); + await selectLayout('Axial Only', 1, false); + await expectImagePixels([nativeColor], 1); + }); + } + + it('preserves embedded RGBA base transparency across axial and oblique views', async () => { + await openVolViewPage( + writeColorImage( + 'base-rgba-alpha', + [...nativeColor, 128], + 'unsigned char', + true + ) + ); + const colors = [0, 128 / 255, 1].map((alpha) => blendedColor(alpha, 0)); + await selectLayout('Axial Only', 1, false); + await expectImagePixels(colors, 1); + await showOblique(); + await expectImagePixels(colors, 3, obliqueSlices); + await selectLayout('Axial Only', 1, false); + await expectImagePixels(colors, 1); + }); + + it('uses native RGB colors with the default layer opacity when no display settings are saved', async () => { + await openOverlayControls('default-rgb-overlay', [180, 0, 0], { + savedDisplay: false, + }); + await expect($('[data-testid="layer-colormap"]')).toHaveText( + expect.stringContaining('2hot-opaque') + ); + await expect( + $('[data-testid="layer-opacity-slider"] [role="slider"]') + ).toHaveAttribute('aria-valuenow', '0.3'); + await expectImagePixels([[144, 90, 90]]); + }); + + for (const { channels, preset, opacityFunction, expected, mode } of [ + { + channels: [180, 0, 0], + preset: '2hot-opaque', + opacityFunction: { + mode: 1, + preset: '2hot-opaque', + shift: 0, + shiftAlpha: 0, + }, + expected: [180, 0, 0], + mode: 'Points', + }, + { + channels: [220, 128], + preset: 'Grayscale', + opacityFunction: { + mode: 2, + nodes: [ + { x: 0, y: 1, midpoint: 0.5, sharpness: 0 }, + { x: 1, y: 1, midpoint: 0.5, sharpness: 0 }, + ], + }, + expected: [174, 174, 174], + mode: 'Nodes', + }, + ]) { + it(`restores a saved ${channels.length}-component layer with ${mode} opacity without losing its rendering path`, async () => { + const base = writeColorImage('legacy-color-base', [128]); + const overlay = writeColorImage('legacy-color-overlay', channels); + await writeManifestToFile( + { + ...layeredImageManifest(base, overlay), + 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' }, + config: { + base: { + window: { + width: 255, + level: 127.5, + auto: 'FullRange', + useAuto: false, + }, + }, + 'base::overlay': { + layers: { + transferFunction: { + preset, + mappingRange: [0, 255], + }, + opacityFunction: { + ...opacityFunction, + mappingRange: [0, 255], + }, + blendConfig: { opacity: 1, visibility: true }, + }, + }, + }, + }, + }, + }, + 'legacy-multicomponent-overlay.json' + ); + await openVolViewPage('legacy-multicomponent-overlay.json'); + await openLayerDisplayControls(); + await expect($('[data-testid="layer-colormap"]')).toHaveText( + expect.stringContaining(preset) + ); + await expectImagePixels([expected], 1); + const visibility = $('[data-testid="layer-visibility"]'); + await visibility.click(); + await expectImagePixels([[128, 128, 128]], 1); + await visibility.click(); + await expectImagePixels([expected], 1); + }); + } +}); diff --git a/tests/specs/overlay-auto-contrast.e2e.ts b/tests/specs/overlay-auto-contrast.e2e.ts new file mode 100644 index 000000000..685d04eff --- /dev/null +++ b/tests/specs/overlay-auto-contrast.e2e.ts @@ -0,0 +1,311 @@ +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { TEMP_DIR } from '../../wdio.shared.conf'; +import { setValueVueInput, volViewPage } from '../pageobjects/volview.page'; +import { + capture2DViewScreenshotMetrics, + makeTempDir, + openVolViewPage, + selectLayout, + SESSION_SAVE_TIMEOUT, + waitForDownload, + waitForModuleTransitions, + writeManifestToFile, + writeMetaImage, +} from './utils'; + +const layer = () => $('section[aria-label="contrast-target.nrrd"]'); +const other = () => $('section[aria-label="contrast-other.mha"]'); +const targetInput = (endpoint: string) => + layer().$(`[data-testid="layer-range-${endpoint}"] input`); +const visibility = () => layer().$('[data-testid="layer-visibility"]'); + +const appliedRange = async () => + Promise.all( + ['min', 'max'].map(async (endpoint) => + Number(await targetInput(endpoint).getAttribute('title')) + ) + ); + +const pixels = () => + capture2DViewScreenshotMetrics('overlay-auto-contrast.png'); + +async function openContrast() { + const trigger = layer().$('[data-testid="layer-contrast"]'); + await trigger.scrollIntoView({ block: 'center', behavior: 'instant' }); + await trigger.click(); + await $('[data-testid="layer-contrast-menu"]').waitForStable(); +} + +async function chooseContrast(option: string) { + await openContrast(); + const item = $(`[data-testid="layer-contrast-${option}"]`); + await expect(item).not.toHaveAttribute('aria-disabled', 'true'); + await item.click(); + await expect($('[data-testid="layer-contrast-menu"]')).not.toBeDisplayed(); +} + +async function unrelatedSettings() { + return { + baseMin: await $('[data-testid="base-image-range-min"] input').getAttribute( + 'title' + ), + baseMax: await $('[data-testid="base-image-range-max"] input').getAttribute( + 'title' + ), + basePalette: await $('[data-testid="base-image-colormap"]').getText(), + palette: await layer().$('[data-testid="layer-colormap"]').getText(), + opacity: await layer() + .$('[data-testid="layer-opacity-slider"] input') + .getValue(), + transparent: await layer() + .$('[data-testid="layer-transparent-background"] input') + .isSelected(), + otherMin: await other() + .$('[data-testid="layer-range-min"] input') + .getAttribute('title'), + otherMax: await other() + .$('[data-testid="layer-range-max"] input') + .getAttribute('title'), + otherPalette: await other().$('[data-testid="layer-colormap"]').getText(), + otherOpacity: await other() + .$('[data-testid="layer-opacity-slider"] input') + .getValue(), + }; +} + +async function openContrastFixture() { + const dir = makeTempDir('overlay-contrast'); + const header = [ + 'NRRD0005', + 'type: short', + 'dimension: 3', + 'sizes: 16 16 16', + 'space: left-posterior-superior', + 'space directions: (2,0,0) (0,2,0) (0,0,2)', + 'space origin: (0,0,0)', + 'encoding: ascii', + ].join('\n'); + const values = Array.from({ length: 4096 }, (_, i) => + i === 0 ? 0 : i === 4095 ? 1000 : 200 + ((i * 17) % 600) + ); + writeFileSync( + join(dir, 'contrast-target.nrrd'), + `${header}\n\n${values.join(' ')}` + ); + const base = writeMetaImage('contrast-base.mha', { size: 32, step: 17 }); + const second = writeMetaImage('contrast-other.mha', { size: 32, step: 29 }); + await writeManifestToFile( + { + version: '7.0.0', + dataSources: [base, 'overlay-contrast/contrast-target.nrrd', second].map( + (name, i) => ({ + id: i + 1, + type: 'uri', + uri: `/tmp/${name}`, + name: name.split('/').at(-1), + }) + ), + datasets: ['base', 'target', 'other'].map((id, i) => ({ + id, + dataSourceId: i + 1, + })), + primarySelection: 'base', + parentToLayers: [ + { selectionKey: 'base', sourceSelectionKeys: ['target', 'other'] }, + ], + layerDisplay: { + 'base::target': { + preset: 'DICOM Hot Iron', + mappingRange: [-2000, -1000], + opacity: 0.8, + transparentBackground: false, + }, + 'base::other': { + preset: 'Cool to Warm', + mappingRange: [20, 230], + opacity: 0.2, + transparentBackground: false, + }, + }, + }, + 'overlay-auto-contrast.json' + ); + await openVolViewPage('overlay-auto-contrast.json'); + await $(`.image-list-card*=${base}`) + .$('[data-testid="dataset-menu-button"]') + .click(); + await $('.v-list-item*=Show in all views').waitForClickable(); + await $('.v-menu.v-overlay--active .v-overlay__content').waitForStable(); + await $('.v-list-item*=Show in all views').click(); + await browser.keys('Escape'); + await volViewPage.renderingModuleTab.click(); + await $('[data-testid="volume-rendering-toggle"]').click(); + await waitForModuleTransitions(); + await browser.waitUntil( + async () => (await $$('.Vue-Toastification__toast').length) === 0 + ); + await expect(targetInput('min')).toHaveAttribute('title', '-2000'); +} + +describe('Overlay automatic contrast', () => { + it('shows contrast choices between the range inputs and applies the full range', async () => { + await openContrastFixture(); + const trigger = layer().$('[data-testid="layer-contrast"]'); + await expect(trigger.$('.mdi-circle-half-full')).toExist(); + await expect(trigger.$('.mdi-menu-down')).toExist(); + const minX = (await targetInput('min').getLocation()).x; + const maxX = (await targetInput('max').getLocation()).x; + const triggerX = (await trigger.getLocation()).x; + expect(triggerX).toBeGreaterThan(minX); + expect(triggerX).toBeLessThan(maxX); + const unchanged = await unrelatedSettings(); + await openContrast(); + expect(await appliedRange()).toEqual([-2000, -1000]); + const menu = $('[data-testid="layer-contrast-menu"]'); + await expect(menu).toHaveText( + 'Full Range\nLow Contrast\nMedium Contrast\nHigh Contrast' + ); + await browser.saveScreenshot( + join(TEMP_DIR, 'overlay-contrast-controls.png') + ); + await expect(menu.$('input')).not.toExist(); + await expect(menu.$('label=Bones')).not.toExist(); + await $('[data-testid="layer-contrast-FullRange"]').click(); + await expect(targetInput('min')).toHaveAttribute('title', '0'); + await expect(targetInput('max')).toHaveAttribute('title', '1000'); + expect(await unrelatedSettings()).toEqual(unchanged); + }); + + it('uses its resampled histogram for progressively narrower contrast in every slice', async () => { + await openContrastFixture(); + const unchanged = await unrelatedSettings(); + await chooseContrast('FullRange'); + const widths: number[] = []; + for (const choice of ['LowContrast', 'MediumContrast', 'HighContrast']) { + await $('header').moveTo(); + const before = await pixels(); + await chooseContrast(choice); + const [min, max] = await appliedRange(); + // Resampling adds over 5% zero padding; source percentiles start above 200. + expect(min).toBe(0); + expect(max).toBeLessThanOrEqual(1000); + expect(max).toBeGreaterThan(min); + widths.push(max - min); + await $('header').moveTo(); + await browser.waitUntil( + async () => { + const after = await pixels(); + return ( + after.length === 3 && + after.every((view, i) => view.checksum !== before[i].checksum) + ); + }, + { + timeoutMsg: + 'Expected overlay contrast to change every slice rendering', + } + ); + expect(await unrelatedSettings()).toEqual(unchanged); + } + expect(widths[0]).toBeGreaterThan(widths[1]); + expect(widths[1]).toBeGreaterThan(widths[2]); + }); + + it('resets edited ranges and invalid drafts without changing other display settings', async () => { + await openContrastFixture(); + const unchanged = await unrelatedSettings(); + await chooseContrast('FullRange'); + await setValueVueInput(targetInput('min'), '-2000'); + await setValueVueInput(targetInput('max'), '-1000'); + await browser.keys('Enter'); + await chooseContrast('FullRange'); + expect(await appliedRange()).toEqual([0, 1000]); + await setValueVueInput(targetInput('min'), '-'); + await browser.keys('Enter'); + await expect( + layer().$('[data-testid="layer-range-status"]') + ).toBeDisplayed(); + await chooseContrast('FullRange'); + await expect(targetInput('min')).toHaveValue('0'); + await expect(targetInput('max')).toHaveValue('1000'); + await expect(layer().$('[data-testid="layer-range-status"]')).not.toExist(); + expect(await unrelatedSettings()).toEqual(unchanged); + }); + + it('hides and restores the overlay in every slice without changing display settings', async () => { + await openContrastFixture(); + const unchanged = await unrelatedSettings(); + await chooseContrast('FullRange'); + await expect(visibility()).toHaveText(expect.stringContaining('HIDE')); + expect((await visibility().getLocation()).x).toBeLessThan( + (await layer().$('[data-testid="layer-colormap"]').getLocation()).x + ); + expect((await visibility().getCSSProperty('border-radius')).value).toBe( + ( + await layer() + .$('[data-testid="layer-contrast"]') + .getCSSProperty('border-radius') + ).value + ); + await expect(visibility().$('.mdi-eye')).toExist(); + await $('header').moveTo(); + const shown = await pixels(); + await visibility().scrollIntoView({ block: 'center', behavior: 'instant' }); + await visibility().click(); + await expect(visibility()).toHaveText(expect.stringContaining('SHOW')); + await expect(visibility().$('.mdi-eye-off')).toExist(); + await $('header').moveTo(); + await browser.waitUntil(async () => { + const hidden = await pixels(); + return ( + hidden.length === 3 && + hidden.every((view, i) => view.checksum !== shown[i].checksum) + ); + }); + expect(await unrelatedSettings()).toEqual(unchanged); + await visibility().click(); + await $('header').moveTo(); + await browser.waitUntil(async () => { + const restored = await pixels(); + return ( + restored.length === 3 && + restored.every((view, i) => view.checksum === shown[i].checksum) + ); + }); + expect(await unrelatedSettings()).toEqual(unchanged); + }); + + it('preserves hidden overlay settings through layout changes and a saved session', async () => { + await openContrastFixture(); + const unchanged = await unrelatedSettings(); + await chooseContrast('FullRange'); + await visibility().scrollIntoView({ block: 'center', behavior: 'instant' }); + await visibility().click(); + await selectLayout('Axial Only', 1, false); + await expect(visibility()).toHaveText(expect.stringContaining('SHOW')); + expect(await unrelatedSettings()).toEqual(unchanged); + const saved = await volViewPage.saveSession(); + await waitForDownload(join(TEMP_DIR, saved), SESSION_SAVE_TIMEOUT); + await openVolViewPage(saved); + await volViewPage.renderingModuleTab.click(); + await visibility().scrollIntoView({ block: 'center', behavior: 'instant' }); + await expect(visibility()).toHaveText(expect.stringContaining('SHOW')); + await selectLayout('Four Up', 3, true); + await expect(visibility()).toHaveText(expect.stringContaining('SHOW')); + expect(await unrelatedSettings()).toEqual(unchanged); + await $('header').moveTo(); + const hidden = await pixels(); + await visibility().click(); + await expect(visibility()).toHaveText(expect.stringContaining('HIDE')); + await $('header').moveTo(); + await browser.waitUntil(async () => { + const restored = await pixels(); + return ( + restored.length === 3 && + restored.every((view, i) => view.checksum !== hidden[i].checksum) + ); + }); + expect(await unrelatedSettings()).toEqual(unchanged); + }); +}); diff --git a/tests/specs/overlay-segmentation-opacity.e2e.ts b/tests/specs/overlay-segmentation-opacity.e2e.ts new file mode 100644 index 000000000..7c4d70a98 --- /dev/null +++ b/tests/specs/overlay-segmentation-opacity.e2e.ts @@ -0,0 +1,148 @@ +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { TEMP_DIR } from '../../wdio.shared.conf'; +import { volViewPage } from '../pageobjects/volview.page'; +import { + openAnnotationSegments, + waitForSegmentContent, +} from './segmentationTestUtils'; +import { + capture2DViewPixels, + selectLayout, + writeManifestToFile, +} from './utils'; + +const writeImages = () => { + const header = [ + 'NRRD0005', + 'type: unsigned char', + 'dimension: 3', + 'sizes: 16 16 16', + 'space: left-posterior-superior', + 'space directions: (1,0,0) (0,1,0) (0,0,1)', + 'space origin: (0,0,0)', + 'encoding: ascii', + ]; + // The viewed plane is white underneath a black overlay. The corner voxel + // preserves a nonzero scalar range without appearing on that plane. + for (const [name, value] of [ + ['opacity-base.nrrd', 255], + ['opacity-base.layer.nrrd', 0], + ] as const) { + const pixels = Array.from({ length: 4096 }, (_, i) => + i === 0 ? 255 - value : value + ).join(' '); + writeFileSync(join(TEMP_DIR, name), `${header.join('\n')}\n\n${pixels}`); + } + const mask = Array.from({ length: 4096 }, (_, index) => { + const i = index % 16; + const j = Math.floor(index / 16) % 16; + const k = Math.floor(index / 256); + return i >= 4 && i <= 11 && j >= 4 && j <= 11 && k >= 4 && k <= 11 ? 1 : 0; + }).join(' '); + writeFileSync( + join(TEMP_DIR, 'opacity-base.seg.nrrd'), + `${header.join('\n')}\nSegment0_LabelValue:=1\nSegment0_Name:=Blue mask\nSegment0_Color:=0 0 1\n\n${mask}` + ); +}; + +const captureSlice = async (fileName: string) => + (await capture2DViewPixels(80, fileName))[0].data; + +describe('Overlay opacity with segmentation', () => { + it('keeps segmentation colors continuous when opacity reaches 100%', async () => { + writeImages(); + await writeManifestToFile( + { + io: { segmentationExtension: 'seg', layerExtension: 'layer' }, + windowing: { width: 255, level: 127.5 }, + }, + 'opacity-config.json' + ); + await volViewPage.open( + '?urls=[tmp/opacity-config.json,tmp/opacity-base.nrrd,tmp/opacity-base.seg.nrrd,tmp/opacity-base.layer.nrrd]' + ); + await volViewPage.waitForViews(); + await selectLayout('Axial Only', 1, false); + await openAnnotationSegments(); + await waitForSegmentContent('Blue mask'); + await volViewPage.renderingModuleTab.click(); + const transparent = $('[data-testid="layer-transparent-background"] input'); + await expect(transparent).toBeChecked(); + await transparent.scrollIntoView({ block: 'center', behavior: 'instant' }); + await transparent.click(); + await expect(transparent).not.toBeChecked(); + const opacity = $('[data-testid="layer-opacity-slider"] [role="slider"]'); + await opacity.scrollIntoView({ block: 'center', behavior: 'instant' }); + await opacity.execute((element) => element.focus()); + await browser.keys(['End']); + await browser.keys(['ArrowLeft']); + await expect( + $('[data-testid="layer-opacity-slider"] [role="slider"]') + ).toHaveAttribute('aria-valuenow', '0.99'); + await $('header').moveTo(); + await browser.execute(async () => { + const transitions = document + .getAnimations() + .filter((animation) => + Number.isFinite(animation.effect?.getComputedTiming().endTime) + ); + await Promise.allSettled( + transitions.map((animation) => animation.finished) + ); + }); + await $('[data-testid~="vtk-two-view"]').waitForStable(); + let below: number[] = []; + let background: number[] = []; + await browser.waitUntil( + async () => { + below = await captureSlice('overlay-opacity-99.png'); + background = []; + let blue = 0; + for (let i = 0; i < below.length; i += 4) { + if ( + below[i + 1] >= 2 && + below[i + 1] <= 3 && + below[i + 1] === below[i + 2] && + below[i] - below[i + 1] <= 16 + ) + background.push(i); + if (below[i + 2] > below[i] + 40) blue++; + } + return background.length > 1000 && blue > 100; + }, + { + timeoutMsg: + 'Expected a blue segmentation and 1% of the white base beneath the black overlay', + } + ); + await opacity.execute((element) => element.focus()); + await browser.keys('End'); + await expect( + $('[data-testid="layer-opacity-slider"] [role="slider"]') + ).toHaveAttribute('aria-valuenow', '1'); + await expect($('[data-testid="layer-opacity-slider"] input')).toHaveValue( + '1' + ); + await $('header').moveTo(); + // At full opacity the white base contributes nothing. Requiring the + // background's green and blue channels to reach zero excludes a stale + // 99% frame, even when the controls update before the next render. + await browser.waitUntil( + async () => { + const full = await captureSlice('overlay-opacity-100.png'); + return ( + full.length === below.length && + background.every( + (index) => full[index + 1] === 0 && full[index + 2] === 0 + ) && + full.every((value, index) => Math.abs(value - below[index]) <= 4) + ); + }, + { + timeoutMsg: + 'Expected 99% to 100% opacity to preserve segmentation colors without a render-pass jump', + } + ); + }); +}); diff --git a/tests/specs/palette-window.e2e.ts b/tests/specs/palette-window.e2e.ts new file mode 100644 index 000000000..ab95191c3 --- /dev/null +++ b/tests/specs/palette-window.e2e.ts @@ -0,0 +1,569 @@ +import { + layeredImageManifest, + openLayerDisplayControls, +} from './layerDisplayTestUtils'; +import { setValueVueInput } from '../pageobjects/volview.page'; +import { + capture2DViewScreenshotMetrics, + captureViewPixels, + openVolViewPage, + writeManifestToFile, + writeMetaImage, +} from './utils'; + +const control = (name: string) => $(`[data-testid="${name}"]`); +const handle = (name: string) => control(`layer-range-${name}-handle`); +const input = (name: string) => control(`layer-range-${name}`).$('input'); +const bar = () => control('palette-window-bar'); +const gradient = async () => + (await bar().$('.color-map-swatch').getCSSProperty('background-image')).value; + +const range = async () => + Promise.all( + ['min', 'max'].map(async (name) => + Number(await handle(name).getAttribute('aria-valuenow')) + ) + ); + +async function openPaletteWindow() { + const base = writeMetaImage('palette-window-base.mha'); + const overlay = writeMetaImage('palette-window-overlay.mha', { step: 29 }); + const manifest = 'palette-window.json'; + await writeManifestToFile( + { + ...layeredImageManifest(base, overlay), + layerDisplay: { + 'base::overlay': { + preset: '2hot', + mappingRange: [50, 200], + opacity: 1, + transparentBackground: false, + }, + }, + }, + manifest + ); + await openVolViewPage(manifest); + const card = $(`.image-list-card*=${base}`); + await card.$('[data-testid="dataset-menu-button"]').click(); + const showInAllViews = $('.v-list-item*=Show in all views'); + await showInAllViews.waitForClickable(); + await showInAllViews.click(); + await browser.keys('Escape'); + await openLayerDisplayControls(); + await bar().waitForDisplayed(); + await bar().scrollIntoView({ block: 'center', behavior: 'instant' }); + await expect(input('min')).toHaveValue('50'); + await expect(input('max')).toHaveValue('200'); +} + +async function drag(name: string, fraction: number) { + const geometry = await browser.execute( + (selector, targetFraction) => { + const track = document + .querySelector('[data-testid="palette-window-bar"]')! + .getBoundingClientRect(); + const thumb = document.querySelector(selector)!.getBoundingClientRect(); + return { + startX: Math.round(thumb.x + thumb.width / 2), + startY: Math.round(thumb.y + thumb.height / 2), + endX: Math.round(track.x + track.width * targetFraction), + }; + }, + `[data-testid="layer-range-${name}-handle"]`, + fraction + ); + await browser.performActions([ + { + type: 'pointer', + id: 'range-pointer', + parameters: { pointerType: 'mouse' }, + actions: [ + { + type: 'pointerMove', + duration: 0, + origin: 'viewport', + x: geometry.startX, + y: geometry.startY, + }, + { type: 'pointerDown', button: 0 }, + { + type: 'pointerMove', + duration: 0, + origin: 'viewport', + x: geometry.endX, + y: geometry.startY, + }, + ], + }, + ]); +} + +async function dragBy(name: string, pixels: number) { + const thumb = handle(name); + const location = await thumb.getLocation(); + const size = await thumb.getSize(); + const x = Math.round(location.x + size.width / 2); + const y = Math.round(location.y + size.height / 2); + const width = (await bar().getSize()).width; + await browser + .action('pointer') + .move({ x, y }) + .down() + .move({ x: x + pixels, y }) + .up() + .perform(); + return (pixels / width) * 255; +} + +const controlRects = () => + browser.execute(() => + [ + '[data-testid="palette-window-bar"]', + '[data-testid="layer-range-min"] input', + '[data-testid="layer-range-max"] input', + ].map((selector) => { + const { x, y, width, height } = document + .querySelector(selector)! + .getBoundingClientRect(); + return { x, y, width, height }; + }) + ); + +async function expectGripsAligned() { + const { barCenter, gripCenters } = await browser.execute(() => { + const barRect = document + .querySelector('[data-testid="palette-window-bar"]')! + .getBoundingClientRect(); + return { + barCenter: barRect.y + barRect.height / 2, + gripCenters: Array.from( + document.querySelectorAll( + '[data-testid="palette-window-bar"] .handle-grip' + ) + ).map((grip) => { + const rect = grip.getBoundingClientRect(); + return rect.y + rect.height / 2; + }), + }; + }); + expect(gripCenters).toHaveLength(3); + for (const center of gripCenters) expect(center).toBeCloseTo(barCenter, 8); +} + +async function expectInputsSynced() { + const [min, max] = await range(); + await expect(input('min')).toHaveAttribute('title', String(min)); + await expect(input('max')).toHaveAttribute('title', String(max)); + expect(min).toBeLessThan(max); + expect(min).toBeGreaterThanOrEqual(0); + expect(max).toBeLessThanOrEqual(255); +} + +async function press(name: string, key: string) { + await browser.execute((selector) => { + document.querySelector(selector)!.focus(); + }, `[data-testid="layer-range-${name}-handle"]`); + await expect(handle(name)).toBeFocused(); + await browser.keys(key); +} + +async function expectRange(min: number, max: number) { + const actual = await range(); + expect(actual[0]).toBeCloseTo(min, 8); + expect(actual[1]).toBeCloseTo(max, 8); + await expectInputsSynced(); +} + +describe('Palette window interaction', () => { + beforeEach(openPaletteWindow); + afterEach(async () => { + await browser.releaseActions(); + }); + + it('restores the original colors after collapsing and reopening the range', async () => { + const capture = () => + captureViewPixels( + '[data-testid~="vtk-two-view"]', + 40, + 'palette-window-reopened.png' + ); + + const selectPalette = async (name: string) => { + await control('layer-colormap').click(); + const option = $(`[role="option"]=${name}`); + await option.scrollIntoView({ block: 'center', behavior: 'instant' }); + await option.waitForClickable(); + await option.click(); + await $('header').moveTo(); + }; + + const fullRange = async () => { + await control('layer-contrast').click(); + await control('layer-contrast-FullRange').waitForClickable(); + await control('layer-contrast-FullRange').click(); + await expectRange(0, 255); + await $('header').moveTo(); + }; + + await selectPalette('Grayscale'); + await fullRange(); + await browser.waitUntil(async () => { + const views = await capture2DViewScreenshotMetrics( + 'palette-window-grayscale.png' + ); + return ( + views.length === 3 && + views.every(({ colored, checksum }) => colored === 0 && checksum > 0) + ); + }); + await selectPalette('DICOM Hot Iron'); + await browser.waitUntil(async () => { + const views = await capture2DViewScreenshotMetrics( + 'palette-window-hot-iron.png' + ); + return views.length === 3 && views.every(({ colored }) => colored > 1000); + }); + const before = await capture(); + const originalGradient = await gradient(); + + await drag('min', 1.1); + await browser.releaseActions(); + const [min, max] = await range(); + expect(min).toBeLessThan(max); + expect(max - min).toBeLessThan(1e-10); + await $('header').moveTo(); + await browser.waitUntil(async () => { + const collapsed = await capture(); + return ( + collapsed.length === 3 && + collapsed.every(({ data }, index) => !data.equals(before[index].data)) + ); + }); + + await fullRange(); + expect(await gradient()).toBe(originalGradient); + await browser.waitUntil( + async () => { + const reopened = await capture(); + return ( + reopened.length === 3 && + reopened.every(({ data }, index) => data.equals(before[index].data)) + ); + }, + { + timeoutMsg: + 'Expected Full Range to restore the original overlay pixels', + } + ); + }); + + it('updates the palette, numbers, and all views while dragging either endpoint', async () => { + const before = await capture2DViewScreenshotMetrics( + 'palette-window-before.png' + ); + expect(before).toHaveLength(3); + const initialGradient = await gradient(); + await drag('min', 0.4); + await browser.waitUntil(async () => (await range())[0] > 90); + await expectInputsSynced(); + expect((await range())[1]).toBe(200); + expect(await gradient()).not.toBe(initialGradient); + await browser.waitUntil( + async () => { + const after = await capture2DViewScreenshotMetrics( + 'palette-window-drag.png' + ); + return ( + after.length === 3 && + after.every((view, index) => view.checksum !== before[index].checksum) + ); + }, + { + timeoutMsg: + 'Expected the dragged range to update every 2D view before pointer release', + } + ); + await browser.releaseActions(); + + const min = (await range())[0]; + await drag('max', 0.6); + await browser.waitUntil(async () => (await range())[1] < 170); + await expectInputsSynced(); + expect((await range())[0]).toBe(min); + await browser.releaseActions(); + + await drag('min', 1); + await expectInputsSynced(); + const collapsedAtMax = await range(); + expect(collapsedAtMax[0]).toBeCloseTo(collapsedAtMax[1], 8); + await browser.releaseActions(); + await drag('min', 0.2); + await expectInputsSynced(); + const reopenedMinimum = await range(); + expect(reopenedMinimum[0]).toBeLessThan(collapsedAtMax[0]); + expect(reopenedMinimum[1]).toBe(collapsedAtMax[1]); + await browser.releaseActions(); + + const expandedRects = await controlRects(); + await expectGripsAligned(); + await drag('max', 0); + await expectInputsSynced(); + expect(await controlRects()).toEqual(expandedRects); + await expectGripsAligned(); + const collapsedAtMin = await range(); + expect(collapsedAtMin[1]).toBeCloseTo(collapsedAtMin[0], 8); + await browser.releaseActions(); + expect(await controlRects()).toEqual(expandedRects); + await expectGripsAligned(); + await drag('max', 0.8); + await expectInputsSynced(); + const reopenedMaximum = await range(); + expect(reopenedMaximum[1]).toBeGreaterThan(collapsedAtMin[1]); + expect(reopenedMaximum[0]).toBe(collapsedAtMin[0]); + expect(await controlRects()).toEqual(expandedRects); + await expectGripsAligned(); + await browser.releaseActions(); + }); + + it('moves the whole window to either boundary without changing its width', async () => { + await drag('center', 0); + await expectRange(0, 150); + await browser.releaseActions(); + await drag('center', 1); + await expectRange(105, 255); + await browser.releaseActions(); + await press('center', 'ArrowRight'); + await expectRange(105, 255); + await drag('center', 0); + await expectRange(0, 150); + await browser.releaseActions(); + await press('center', 'ArrowLeft'); + await expectRange(0, 150); + await press('center', 'ArrowRight'); + await expectRange(2.55, 152.55); + }); + + it('matches native hover feedback, shows keyboard halos and smaller directional circles for values outside the image', async () => { + const normalSize = await handle('min').$('.handle-grip').getSize(); + const supportsHover = await browser.execute( + () => matchMedia('(hover: hover)').matches + ); + const opacitySlider = control('layer-opacity-slider').$('[role="slider"]'); + await opacitySlider.moveTo(); + const nativeHalo = await opacitySlider + .$('.v-slider-thumb__surface') + .execute((element) => ({ + opacity: Number(getComputedStyle(element, '::before').opacity), + hoverOpacity: Number( + getComputedStyle(element).getPropertyValue('--v-hover-opacity') + ), + })); + if ( + !nativeHalo || + typeof nativeHalo !== 'object' || + !('opacity' in nativeHalo) || + !('hoverOpacity' in nativeHalo) + ) + throw new Error('Expected native slider halo styles'); + const expectedHoverOpacity = supportsHover + ? Number(nativeHalo.hoverOpacity) + : 0; + for (const name of ['min', 'max', 'center']) { + await handle(name).moveTo(); + await browser.waitUntil(async () => + handle(name) + .$('.handle-grip') + .execute( + (element, opacity) => + Math.abs( + Number(getComputedStyle(element, '::before').opacity) - + Number(opacity) + ) < 0.001, + expectedHoverOpacity + ) + ); + expect( + await handle(name).execute((element) => element.matches(':hover')) + ).toBe(true); + } + if (!supportsHover) expect(Number(nativeHalo.opacity)).toBe(0); + for (const name of ['min', 'max', 'center']) { + await handle(name).execute((element) => element.focus()); + await browser.keys('Tab'); + await browser.keys(['Shift', 'Tab']); + await expect(handle(name)).toBeFocused(); + await browser.waitUntil(async () => + handle(name) + .$('.handle-grip') + .execute( + (element) => + Number(getComputedStyle(element, '::before').opacity) > 0 + ) + ); + } + await setValueVueInput(input('min'), '-100'); + await setValueVueInput(input('max'), '355'); + await browser.keys('Enter'); + for (const [name, direction] of [ + ['min', 'left'], + ['max', 'right'], + ]) { + const grip = handle(name).$('.handle-grip'); + await expect(grip).toHaveAttribute('data-direction', direction); + const size = await grip.getSize(); + expect(size.width).toBeLessThan(normalSize.width); + expect(size.width).toBe(size.height); + } + await expectGripsAligned(); + await setValueVueInput(input('min'), '-400'); + await setValueVueInput(input('max'), '-200'); + await browser.keys('Enter'); + await expect(handle('center').$('.handle-grip')).toHaveAttribute( + 'data-direction', + 'left' + ); + const outsideCenter = await handle('center').$('.handle-grip').getSize(); + expect(outsideCenter.width).toBeLessThan(normalSize.width); + expect(outsideCenter.width).toBe(outsideCenter.height); + await expectGripsAligned(); + }); + + it('moves out-of-range endpoints continuously from their actual values', async () => { + await setValueVueInput(input('min'), '-100'); + await setValueVueInput(input('max'), '100'); + await browser.keys('Enter'); + const fixedAxis = await controlRects(); + const minDelta = await dragBy('min', 2); + const left = await range(); + expect(left[0]).toBeCloseTo(-100 + minDelta, 8); + expect(left[0]).toBeLessThan(0); + expect(left[1]).toBe(100); + expect(await controlRects()).toEqual(fixedAxis); + await expectGripsAligned(); + + await setValueVueInput(input('max'), '355'); + await setValueVueInput(input('min'), '155'); + await browser.keys('Enter'); + const maxDelta = await dragBy('max', -2); + const right = await range(); + expect(right[0]).toBe(155); + expect(right[1]).toBeCloseTo(355 + maxDelta, 8); + expect(right[1]).toBeGreaterThan(255); + expect(await controlRects()).toEqual(fixedAxis); + await expectGripsAligned(); + }); + + it('moves a wider-than-image window and an offscreen center without changing width', async () => { + await setValueVueInput(input('min'), '-100'); + await setValueVueInput(input('max'), '355'); + await browser.keys('Enter'); + await expect(handle('center')).toBeEnabled(); + const wideDelta = await dragBy('center', 2); + const wide = await range(); + expect(wide[0]).toBeCloseTo(-100 + wideDelta, 8); + expect(wide[1]).toBeCloseTo(355 + wideDelta, 8); + expect(wide[1] - wide[0]).toBeCloseTo(455, 8); + + await setValueVueInput(input('min'), '-400'); + await setValueVueInput(input('max'), '-200'); + await browser.keys('Enter'); + await expect(handle('center')).toBeEnabled(); + const outsideDelta = await dragBy('center', 2); + const outside = await range(); + expect(outside[0]).toBeCloseTo(-400 + outsideDelta, 8); + expect(outside[1]).toBeCloseTo(-200 + outsideDelta, 8); + expect(outside[1] - outside[0]).toBeCloseTo(200, 8); + await expectGripsAligned(); + }); + + it('recovers an offset full-width window and keeps its center interactive at the bounds', async () => { + await setValueVueInput(input('min'), '-10'); + await browser.keys('Enter'); + await setValueVueInput(input('max'), '245'); + await browser.keys('Enter'); + await expect(input('min')).toHaveAttribute('title', '-10'); + await expect(input('max')).toHaveAttribute('title', '245'); + await expect(handle('center')).toBeEnabled(); + await drag('center', 1); + await expectRange(0, 255); + await browser.releaseActions(); + await expect(handle('center')).toBeEnabled(); + await drag('center', 0); + await expectRange(0, 255); + await browser.releaseActions(); + await drag('center', 1); + await expectRange(0, 255); + await browser.releaseActions(); + await expect(handle('center')).toBeEnabled(); + }); + + it('tabs through handles, supports arrow steps, and preserves precise manual input', async () => { + await control('layer-colormap') + .$('input') + .execute((element) => element.focus()); + await browser.keys('Tab'); + await expect(handle('min')).toBeFocused(); + await expectRange(50, 200); + await browser.keys('ArrowUp'); + await expectRange(52.55, 200); + await browser.keys('ArrowDown'); + await expectRange(50, 200); + + await browser.keys('Tab'); + await expect(handle('max')).toBeFocused(); + await browser.keys('ArrowLeft'); + await expectRange(50, 197.45); + await browser.keys('ArrowRight'); + await expectRange(50, 200); + + await browser.keys('Tab'); + await expect(handle('center')).toBeFocused(); + await browser.keys('ArrowRight'); + await expectRange(52.55, 202.55); + await browser.keys('ArrowLeft'); + await expectRange(50, 200); + for (const key of ['PageUp', 'PageDown', 'Home', 'End']) { + await browser.keys(key); + await expectRange(50, 200); + } + + await browser.keys('Tab'); + await expect(input('min')).toBeFocused(); + for (const name of ['center', 'max', 'min']) { + await browser.keys(['Shift', 'Tab']); + await expect(handle(name)).toBeFocused(); + } + await expectRange(50, 200); + + await setValueVueInput(input('min'), '0'); + await setValueVueInput(input('max'), '1'); + await browser.keys('Enter'); + await press('min', 'ArrowRight'); + await expectInputsSynced(); + const minimumBound = await range(); + expect(minimumBound[0]).toBeGreaterThan(0); + expect(minimumBound[1]).toBe(1); + await press('min', 'ArrowLeft'); + await press('max', 'ArrowLeft'); + await expectInputsSynced(); + const maximumBound = await range(); + expect(maximumBound[0]).toBe(0); + expect(maximumBound[1]).toBe(Number.MIN_VALUE); + + await setValueVueInput(input('min'), '-2.123456789'); + await browser.keys('Enter'); + await setValueVueInput(input('max'), '100.123456789'); + await browser.keys('Enter'); + await $('h4*=palette-window-overlay.mha').click(); + await expect(input('min')).toHaveAttribute('title', '-2.123456789'); + await expect(input('max')).toHaveAttribute('title', '100.123456789'); + await setValueVueInput(input('max'), '-3'); + await browser.keys('Enter'); + await expect(control('layer-range-status')).toHaveText( + 'Min must be less than Max.' + ); + await expect(input('min')).toHaveAttribute('title', '-2.123456789'); + await expect(input('max')).toHaveAttribute('title', '100.123456789'); + }); +}); diff --git a/tests/specs/range-number-steppers.e2e.ts b/tests/specs/range-number-steppers.e2e.ts new file mode 100644 index 000000000..2b6f55514 --- /dev/null +++ b/tests/specs/range-number-steppers.e2e.ts @@ -0,0 +1,184 @@ +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { setValueVueInput } from '../pageobjects/volview.page'; +import { + layeredImageManifest, + openLayerDisplayControls, +} from './layerDisplayTestUtils'; +import { makeTempDir, openVolViewPage, writeManifestToFile } from './utils'; + +async function openRanges(overlayRange: [number, number]) { + const directory = makeTempDir('number-steppers'); + const files = [[-1000, 500], overlayRange].map((range, image) => { + const name = `steppers-${image}.nrrd`; + const header = [ + 'NRRD0005', + 'type: double', + 'dimension: 3', + 'sizes: 16 16 16', + 'space: left-posterior-superior', + 'space directions: (1,0,0) (0,1,0) (0,0,1)', + 'space origin: (0,0,0)', + 'encoding: ascii', + ].join('\n'); + const values = Array.from({ length: 4096 }, (_, i) => range[i % 2]).join( + ' ' + ); + writeFileSync(join(directory, name), `${header}\n\n${values}`); + return `number-steppers/${name}`; + }); + await writeManifestToFile( + { + ...layeredImageManifest(files[0], files[1]), + layerDisplay: { + 'base::overlay': { + preset: 'DICOM Hot Iron', + mappingRange: overlayRange, + opacity: 0.5, + transparentBackground: false, + }, + }, + }, + 'number-steppers.json' + ); + await openVolViewPage('number-steppers.json'); + await $('.image-list-card*=steppers-0.nrrd') + .$('[data-testid="dataset-menu-button"]') + .click(); + const show = $('.v-list-item*=Show in all views'); + await show.waitForClickable(); + await show.click(); + await browser.keys('Escape'); + await openLayerDisplayControls(); +} + +const control = (id: string) => $(`[data-testid="${id}"]`); +const input = (prefix: string, endpoint: string) => + control(`${prefix}-${endpoint}`).$('input'); + +async function expectEndpoint( + prefix: string, + endpoint: string, + expected: number, + tolerance: number +) { + await browser.waitUntil( + async () => + Math.abs( + Number(await input(prefix, endpoint).getAttribute('title')) - expected + ) < tolerance + ); +} + +async function reversibleStep(prefix: string, endpoint: string, step: number) { + const field = input(prefix, endpoint); + const other = input(prefix, endpoint === 'min' ? 'max' : 'min'); + const original = Number(await field.getAttribute('title')); + const unchanged = await other.getAttribute('title'); + await field.scrollIntoView({ block: 'center', behavior: 'instant' }); + await control(`${prefix}-${endpoint}`).moveTo(); + const increase = control(`${prefix}-${endpoint}-increase`); + const decrease = control(`${prefix}-${endpoint}-decrease`); + expect( + Number((await increase.parentElement().getCSSProperty('opacity')).value) + ).toBe(1); + await increase.click(); + await expectEndpoint(prefix, endpoint, original + step, step / 10000); + await decrease.click(); + await expectEndpoint(prefix, endpoint, original, step / 10000); + await field.click(); + await $('header').moveTo(); + expect( + Number((await increase.parentElement().getCSSProperty('opacity')).value) + ).toBe(1); + await browser.keys('ArrowUp'); + await expectEndpoint(prefix, endpoint, original + step, step / 10000); + await browser.keys('ArrowDown'); + await expectEndpoint(prefix, endpoint, original, step / 10000); + expect(await other.getAttribute('title')).toBe(unchanged); +} + +describe('Display range number steppers', () => { + it('uses underlined manual window fields and reversible negative and fractional endpoint steps', async () => { + await openRanges([0, 1]); + await control('base-image-contrast').click(); + const manual = $('.v-menu.v-overlay--active .manual-inputs'); + await manual.waitForDisplayed(); + await manual.waitForStable(); + const fields = await manual.$$('.v-field'); + expect(fields).toHaveLength(2); + for (const field of fields) { + await expect(field).toHaveElementClass('v-field--variant-underlined'); + expect((await field.getCSSProperty('background-color')).value).toBe( + 'rgba(0,0,0,0)' + ); + } + const values = await manual.$$('input'); + await setValueVueInput(values[0], '600'); + await setValueVueInput(values[1], '-200'); + await browser.keys('Enter'); + await browser.keys('Escape'); + await expectEndpoint('base-image-range', 'min', -500, 1e-10); + await expectEndpoint('base-image-range', 'max', 100, 1e-10); + await reversibleStep('base-image-range', 'min', 1); + await reversibleStep('layer-range', 'max', 0.01); + }); + + it('steps tiny floating image ranges without rounding their values or applying incomplete drafts', async () => { + await openRanges([1e-9, 9e-9]); + await reversibleStep('layer-range', 'min', 1e-11); + await reversibleStep('layer-range', 'max', 1e-11); + const minimum = input('layer-range', 'min'); + const applied = await minimum.getAttribute('title'); + const maximum = await input('layer-range', 'max').getAttribute('title'); + await setValueVueInput(minimum, '1e-'); + await expect(control('layer-range-status')).toHaveText( + 'Min and Max must be finite numbers.' + ); + await expect(control('layer-range-min-increase')).toBeDisabled(); + await expect(control('layer-range-min-decrease')).toBeDisabled(); + await browser.keys(['ArrowUp', 'ArrowDown']); + await expect(minimum).toHaveValue('1e-'); + expect(await minimum.getAttribute('title')).toBe(applied); + expect(await input('layer-range', 'max').getAttribute('title')).toBe( + maximum + ); + }); + + it('steps large off-image endpoints with buttons and keyboard for base and overlay ranges', async () => { + await openRanges([0, 1]); + const minimum = 1e16; + const maximum = minimum + 100; + for (const prefix of ['base-image-range', 'layer-range']) { + for (const endpoint of ['min', 'max']) { + await setValueVueInput(input(prefix, 'max'), String(maximum)); + await setValueVueInput(input(prefix, 'min'), String(minimum)); + await $('header').click(); + await reversibleStep(prefix, endpoint, 4); + } + } + }); + + it('shows tiny Contrast values and preserves full precision when editing the other manual field', async () => { + await openRanges([0, 1]); + await setValueVueInput(input('base-image-range', 'min'), '0'); + await setValueVueInput(input('base-image-range', 'max'), '0.0001'); + await control('base-image-contrast').click(); + const manual = $('.v-menu.v-overlay--active .manual-inputs'); + await manual.waitForDisplayed(); + const fields = await manual.$$('input'); + await expect(fields[0]).toHaveValue('0.0001'); + await expect(fields[1]).toHaveValue('0.00005'); + await setValueVueInput(fields[0], '1.23456789e-8'); + await setValueVueInput(fields[1], '2.34567891e-8'); + await browser.keys('Escape'); + await expectEndpoint('base-image-range', 'min', 1.728394965e-8, 1e-22); + await expectEndpoint('base-image-range', 'max', 2.962962855e-8, 1e-22); + await control('base-image-contrast').click(); + await expect(fields[0]).toHaveValue('1.235e-8'); + await expect(fields[1]).toHaveValue('2.346e-8'); + await browser.keys('Escape'); + await expectEndpoint('base-image-range', 'min', 1.728394965e-8, 1e-22); + await expectEndpoint('base-image-range', 'max', 2.962962855e-8, 1e-22); + }); +}); diff --git a/tests/specs/transparent-background.e2e.ts b/tests/specs/transparent-background.e2e.ts new file mode 100644 index 000000000..79282165c --- /dev/null +++ b/tests/specs/transparent-background.e2e.ts @@ -0,0 +1,260 @@ +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { TEMP_DIR } from '../../wdio.shared.conf'; +import { setValueVueInput, volViewPage } from '../pageobjects/volview.page'; +import { capture2DViewPixels, writeManifestToFile } from './utils'; + +function writeImages() { + const header = [ + 'NRRD0005', + 'type: float', + 'dimension: 3', + 'sizes: 32 32 32', + 'space: left-posterior-superior', + 'space directions: (1,0,0) (0,1,0) (0,0,1)', + 'space origin: (0,0,0)', + 'encoding: ascii', + ].join('\n'); + for (const overlay of [false, true]) { + const values = Array.from({ length: 32 ** 3 }, (_, index) => { + const coordinates = [ + index % 32, + Math.floor(index / 32) % 32, + Math.floor(index / 1024), + ]; + const sum = (size: number) => + coordinates.reduce( + (total, coordinate) => total + Math.floor(coordinate / size), + 0 + ); + return overlay + ? [-3.25, -2.75, -2.25, 2.25][sum(8) % 4] + : [80, 180][sum(4) % 2]; + }); + const name = overlay + ? 'transparent-base.layer.nrrd' + : 'transparent-base.nrrd'; + writeFileSync(join(TEMP_DIR, name), `${header}\n\n${values.join(' ')}`); + } +} + +const captureViews = () => capture2DViewPixels(50, 'transparent-capture.png'); + +type ViewPixels = Awaited>; + +function matchingPixels(base: ViewPixels, opaque: ViewPixels) { + return base.map((view, viewIndex) => { + const full = opaque[viewIndex]; + const groups: number[][] = [[], [], []]; + for ( + let i = (view.width + 1) * 4; + i < view.data.length - (view.width + 1) * 4; + i += 4 + ) { + const gray = view.data[i]; + const color = full.data.slice(i, i + 3); + const [red, green, blue] = color; + const group = [ + red <= 1 && green <= 1 && blue <= 1, + red >= 24 && red <= 28 && green <= 2 && blue <= 2, + red >= 253 && green <= 2 && blue <= 2, + ].findIndex(Boolean); + if (group < 0 || ![80, 180].some((value) => Math.abs(gray - value) <= 1)) + continue; + const same = [ + -view.width - 1, + -view.width, + -view.width + 1, + -1, + 0, + 1, + view.width - 1, + view.width, + view.width + 1, + ].every((offset) => + [0, 1, 2].every( + (channel) => + view.data[i + offset * 4 + channel] === gray && + full.data[i + offset * 4 + channel] === color[channel] + ) + ); + if (same) groups[group].push(i); + } + return groups; + }); +} + +function matchesBlend( + base: ViewPixels, + opaque: ViewPixels, + actual: ViewPixels, + groups: number[][][], + alphas: number[] +) { + return ( + actual.length === 3 && + actual.every((view, viewIndex) => + groups[viewIndex].every( + (indices, group) => + indices.length > 100 && + indices.every((index) => + [0, 1, 2].every((channel) => { + const expected = + base[viewIndex].data[index + channel] * (1 - alphas[group]) + + opaque[viewIndex].data[index + channel] * alphas[group]; + return Math.abs(view.data[index + channel] - expected) <= 2; + }) + ) + ) + ) + ); +} + +async function setOpacity(percent: number) { + const slider = $('[data-testid="layer-opacity-slider"] [role="slider"]'); + await slider.scrollIntoView({ block: 'center', behavior: 'instant' }); + await slider.execute((element) => element.focus()); + await browser.keys('Home'); + for (let value = 0; value < percent; value += 10) + await browser.keys('PageUp'); + await expect( + $('[data-testid="layer-opacity-slider"] [role="slider"]') + ).toHaveAttribute('aria-valuenow', String(percent / 100)); + await $('header').moveTo(); +} + +describe('Transparent overlay background', () => { + it('preserves anatomy below Min and fades only the first 10% of a signed fractional range in every view', async () => { + writeImages(); + await writeManifestToFile( + { + io: { layerExtension: 'layer' }, + windowing: { width: 255, level: 127.5 }, + }, + 'transparent-config.json' + ); + await volViewPage.open( + '?urls=[tmp/transparent-config.json,tmp/transparent-base.nrrd,tmp/transparent-base.layer.nrrd]' + ); + await volViewPage.waitForViews(); + await volViewPage.waitForViewCounts(3, true); + await volViewPage.renderingModuleTab.click(); + for (const title of ['Color Presets', 'Cinematic Rendering']) { + const section = $(`.v-expansion-panel-title*=${title}`); + if ((await section.getAttribute('aria-expanded')) === 'true') + await section.click(); + } + const toggle = $('[data-testid="layer-transparent-background"] input'); + await expect(toggle).toBeChecked(); + await expect($('[data-testid="layer-colormap"]')).toHaveText( + expect.stringContaining('DICOM Hot Iron') + ); + await browser.waitUntil( + async () => (await $$('.Vue-Toastification__toast').length) === 0 + ); + await $('[data-testid="layer-range-min"] input').scrollIntoView({ + block: 'center', + behavior: 'instant', + }); + await setValueVueInput($('[data-testid="layer-range-min"] input'), '-2.75'); + await setValueVueInput($('[data-testid="layer-range-max"] input'), '7.25'); + await setOpacity(0); + let base: ViewPixels = []; + await browser.waitUntil( + async () => { + base = await captureViews(); + return ( + base.length === 3 && + base.every(({ data }) => + [80, 180].every( + (gray) => + data.filter( + (value, index) => + index % 4 === 0 && + Math.abs(value - gray) <= 1 && + data[index + 1] === value && + data[index + 2] === value + ).length > 500 + ) + ) + ); + }, + { + timeoutMsg: + 'Expected both gray anatomy regions in every view at zero overlay opacity', + } + ); + await toggle.scrollIntoView({ block: 'center', behavior: 'instant' }); + await toggle.click(); + await expect(toggle).not.toBeChecked(); + await setOpacity(100); + let opaque: ViewPixels = []; + let groups: number[][][] = []; + await browser.waitUntil( + async () => { + opaque = await captureViews(); + groups = matchingPixels(base, opaque); + return ( + groups.length === 3 && + groups.every((view) => view.every((indices) => indices.length > 100)) + ); + }, + { + timeoutMsg: + 'Expected the below-Min, halfway-fade and above-fade regions in every view', + } + ); + await setOpacity(50); + await browser.waitUntil( + async () => + matchesBlend( + base, + opaque, + await captureViews(), + groups, + [0.5, 0.5, 0.5] + ), + { + timeoutMsg: + 'Expected 50% straight blending while transparent background is off', + } + ); + await toggle.scrollIntoView({ block: 'center', behavior: 'instant' }); + await toggle.click(); + await expect(toggle).toBeChecked(); + await $('header').moveTo(); + await browser.waitUntil( + async () => + matchesBlend( + base, + opaque, + await captureViews(), + groups, + [0, 0.25, 0.5] + ), + { + timeoutMsg: + 'Expected unchanged anatomy below Min, half the selected opacity halfway through the fade, and selected opacity above it in all views', + } + ); + await toggle.scrollIntoView({ block: 'center', behavior: 'instant' }); + await toggle.click(); + await expect(toggle).not.toBeChecked(); + await $('header').moveTo(); + await browser.waitUntil( + async () => + matchesBlend( + base, + opaque, + await captureViews(), + groups, + [0.5, 0.5, 0.5] + ), + { + timeoutMsg: + 'Expected switching transparent background off to restore straight blending in all views', + } + ); + expect(await volViewPage.getNotificationsCount()).toBe(0); + }); +}); diff --git a/tests/specs/utils.ts b/tests/specs/utils.ts index 7270dc753..d4b040fed 100644 --- a/tests/specs/utils.ts +++ b/tests/specs/utils.ts @@ -2,10 +2,135 @@ import * as path from 'path'; import * as fs from 'fs'; import { cleanuptotal } from 'wdio-cleanuptotal-service'; import JSZip from 'jszip'; +import { PNG } from 'pngjs'; import { TEMP_DIR } from '../../wdio.shared.conf'; import { volViewPage } from '../pageobjects/volview.page'; import type { TestDataset } from '../datasets'; +export async function captureViewPixels( + selector: string, + inset: number, + fileName: string +) { + const screenshot = PNG.sync.read( + await browser.saveScreenshot(path.join(TEMP_DIR, fileName)) + ); + const geometry = await browser.execute( + (viewSelector) => ({ + width: window.innerWidth, + height: window.innerHeight, + views: Array.from(document.querySelectorAll(viewSelector)).map((view) => { + const rect = view.getBoundingClientRect(); + return { + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + }; + }), + }), + selector + ); + const scaleX = screenshot.width / geometry.width; + const scaleY = screenshot.height / geometry.height; + return geometry.views.map((rect) => { + const x = Math.trunc((rect.x + inset) * scaleX); + const y = Math.trunc((rect.y + inset) * scaleY); + const width = Math.trunc((rect.width - 2 * inset) * scaleX); + const height = Math.trunc((rect.height - 2 * inset) * scaleY); + if ( + width <= 0 || + height <= 0 || + x < 0 || + y < 0 || + x + width > screenshot.width || + y + height > screenshot.height + ) + throw new Error( + `Expected a nonempty view crop inside the screenshot: ${selector}` + ); + const data = Buffer.alloc(width * height * 4); + for (let row = 0; row < height; row++) { + const source = ((y + row) * screenshot.width + x) * 4; + screenshot.data.copy(data, row * width * 4, source, source + width * 4); + } + return { width, data }; + }); +} + +export async function capture2DViewPixels(inset: number, fileName: string) { + const views = await captureViewPixels( + '[data-testid~="vtk-two-view"]', + inset, + fileName + ); + return views.map(({ width, data }) => ({ + width, + data: Array.from(data), + })); +} + +export async function capture2DViewScreenshotMetrics(fileName: string) { + const views = await captureViewPixels( + '[data-testid~="vtk-two-view"]', + 40, + fileName + ); + return views.map(({ data }) => { + let colored = 0; + let blue = 0; + let checksum = 0; + for (let i = 0; i < data.length; i += 4) { + const red = data[i]; + const green = data[i + 1]; + const blueChannel = data[i + 2]; + if ( + Math.max(red, green, blueChannel) - Math.min(red, green, blueChannel) > + 40 + ) + colored++; + if (blueChannel > red + 40 && blueChannel > green + 40) blue++; + checksum += red + 3 * green + 7 * blueChannel; + } + return { colored, blue, checksum }; + }); +} + +export async function waitForModuleTransitions() { + await browser.waitUntil( + () => + browser.execute(() => { + const container = document.getElementById('module-container')!; + const transitioning = container.querySelector( + '[class*="-enter-active"], [class*="-leave-active"]' + ); + const animating = container + .getAnimations({ subtree: true }) + .some( + (animation) => + animation.playState === 'running' && + Number.isFinite(animation.effect?.getComputedTiming().endTime) + ); + return !transitioning && !animating; + }), + { + timeoutMsg: 'Expected the module panels to finish moving', + } + ); +} + +export async function selectLayout( + name: string, + count2D: number, + has3D: boolean +) { + await volViewPage.layoutButton.click(); + const option = $(`.v-list-item-title=${name}`); + await option.waitForClickable(); + await option.click(); + await volViewPage.waitForViewCounts(count2D, has3D); +} + export function writeMetaImage( fileName: string, { spacing = '1 1 1', size = 16, step = 1 } = {} From ee9f8f5bf89ce05916530fdc7968ec3194479ab3 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Thu, 22 Oct 2026 11:16:36 -0400 Subject: [PATCH 3/5] fix(rendering): preserve slice state across layout changes --- src/components/vtk/VtkSliceView.vue | 26 +- .../__tests__/useAutoFitState.spec.ts | 8 +- src/composables/useAutoFitState.ts | 11 +- src/composables/useSyncWindowing.ts | 12 +- src/io/state-file/schema.ts | 1 + src/store/view-configs/camera.ts | 1 + src/store/view-configs/types.ts | 5 +- src/store/view-configs/windowing.ts | 1 + tests/specs/layer-display-controls.e2e.ts | 18 +- tests/specs/slice-display-lifecycle.e2e.ts | 339 ++++++++++++++++++ 10 files changed, 402 insertions(+), 20 deletions(-) create mode 100644 tests/specs/slice-display-lifecycle.e2e.ts diff --git a/src/components/vtk/VtkSliceView.vue b/src/components/vtk/VtkSliceView.vue index 8b65a7ca8..c9f0e3b57 100644 --- a/src/components/vtk/VtkSliceView.vue +++ b/src/components/vtk/VtkSliceView.vue @@ -1,5 +1,13 @@