diff --git a/src/components/VolumeRendering.vue b/src/components/VolumeRendering.vue index f181a71ce..0e71385f2 100644 --- a/src/components/VolumeRendering.vue +++ b/src/components/VolumeRendering.vue @@ -7,6 +7,7 @@ import { watch, watchEffect, } from 'vue'; +import { onKeyDown, onKeyUp } from '@vueuse/core'; import { PresetNameList } from '@/src/vtk/ColorMaps'; import vtkPiecewiseWidget from '@/src/vtk/PiecewiseWidget'; import type { vtkSubscription } from '@kitware/vtk.js/interfaces'; @@ -119,6 +120,7 @@ export default defineComponent({ { mode, shift: pwfWidget.getOpacityPointShift(), + shiftAlpha: pwfWidget.getOpacityValueShift(), } ); } @@ -227,9 +229,10 @@ export default defineComponent({ const points = getShiftedOpacityFromPreset( opFunc.preset, opFunc.mappingRange, + 0, 0 ); - pwfWidget.setOpacityPoints(points, opFunc.shift); + pwfWidget.setOpacityPoints(points, opFunc.shift, opFunc.shiftAlpha); } }, { immediate: true } @@ -279,6 +282,9 @@ export default defineComponent({ const rangeShift = ref(0); const rangeWidth = ref(0); + onKeyDown('Control', () => pwfWidget.setShiftOpacityValues(true)); + onKeyUp('Control', () => pwfWidget.setShiftOpacityValues(false)); + // reset case watch( [selectedPreset, currentImageID], diff --git a/src/components/VtkThreeView.vue b/src/components/VtkThreeView.vue index 1e7d17f01..0735cfdd7 100644 --- a/src/components/VtkThreeView.vue +++ b/src/components/VtkThreeView.vue @@ -361,7 +361,8 @@ function useColoringEffect( const opacityPoints = getShiftedOpacityFromPreset( opacityFunc.preset, opacityFunc.mappingRange, - opacityFunc.shift + opacityFunc.shift, + opacityFunc.shiftAlpha ); if (opacityPoints) { pwf.setPoints(opacityPoints); diff --git a/src/components/VtkTwoView.vue b/src/components/VtkTwoView.vue index a3a9e7ba8..85986d3aa 100644 --- a/src/components/VtkTwoView.vue +++ b/src/components/VtkTwoView.vue @@ -823,7 +823,8 @@ export default defineComponent({ const opacityPoints = getShiftedOpacityFromPreset( opFunc.preset, opFunc.mappingRange, - opFunc.shift + opFunc.shift, + opFunc.shiftAlpha ); if (opacityPoints) { pwf.setPoints(opacityPoints); diff --git a/src/io/state-file/schema.ts b/src/io/state-file/schema.ts index 53877bfc6..34acadb23 100644 --- a/src/io/state-file/schema.ts +++ b/src/io/state-file/schema.ts @@ -153,6 +153,7 @@ const OpacityPoints = z.object({ mode: z.literal(vtkPiecewiseFunctionProxy.Mode.Points), preset: z.string(), shift: z.number(), + shiftAlpha: z.number(), mappingRange: z.tuple([z.number(), z.number()]), }) satisfies z.ZodType; diff --git a/src/types/views.ts b/src/types/views.ts index 53535d925..bae31dd00 100644 --- a/src/types/views.ts +++ b/src/types/views.ts @@ -27,6 +27,7 @@ export interface OpacityPoints { // base preset that has the opacity points preset: string; shift: number; + shiftAlpha: number; mappingRange: [number, number]; } diff --git a/src/utils/vtk-helpers.ts b/src/utils/vtk-helpers.ts index deae3f77a..bb4d86b9c 100644 --- a/src/utils/vtk-helpers.ts +++ b/src/utils/vtk-helpers.ts @@ -116,7 +116,8 @@ export function getCSSCoordinatesFromEvent(eventData: any) { export function getShiftedOpacityFromPreset( presetName: string, effectiveRange: [number, number], - shift: number + shift: number, + shiftAlpha: number ) { const preset = vtkColorMaps.getPresetByName(presetName); if (preset.OpacityPoints) { @@ -128,7 +129,13 @@ export function getShiftedOpacityFromPreset( const [xmin, xmax] = effectiveRange; const width = xmax - xmin; - return points.map(([x, y]) => [(x - xmin) / width + shift, y]); + return points.map(([x, y]) => { + // Non-zero values should be affected by shift + // but preset values of zero should not + const shifted = y && y - shiftAlpha; + const yVal = Math.max(Math.min(shifted, 1), 0); + return [(x - xmin) / width + shift, yVal]; + }); } return null; } @@ -146,6 +153,7 @@ export function getOpacityFunctionFromPreset( mode: vtkPiecewiseFunctionProxy.Mode.Points, preset: presetName, shift: 0, + shiftAlpha: 0, }; } return { diff --git a/src/vtk/PiecewiseWidget/index.js b/src/vtk/PiecewiseWidget/index.js index 3f699b598..88d513eff 100644 --- a/src/vtk/PiecewiseWidget/index.js +++ b/src/vtk/PiecewiseWidget/index.js @@ -16,7 +16,12 @@ export const ADJUST_POSITION_CURSOR = '-webkit-grab'; * Assumes normalized x/y coordinate points. */ -export function samplePiecewiseLinear(points, shift = 0, samples = 256) { +export function samplePiecewiseLinear( + points, + shift = 0, + shiftAlpha = 0, + samples = 256 +) { const filledPoints = [...points]; // set endpoints to drop to 0 filledPoints.push([points[0][0], 0]); @@ -47,7 +52,11 @@ export function samplePiecewiseLinear(points, shift = 0, samples = 256) { } else if (slope === -Infinity) { sampledPoints.push(0); } else { - sampledPoints.push(slope * (sx - p1[0]) + p1[1]); + // Non-zero values should be affected by shift + // but original values of zero should not + let value = slope * (sx - p1[0]) + p1[1]; + value = value && value - shiftAlpha; + sampledPoints.push(value); } } } @@ -66,6 +75,7 @@ function vtkPiecewiseWidget(publicAPI, model) { model.pwMode = Mode.Gaussians; model.opacityPoints = []; model.opacityPointShift = 0; + model.opacityValueShift = 0; publicAPI.setGaussiansMode = () => { model.pwMode = Mode.Gaussians; @@ -82,8 +92,13 @@ function vtkPiecewiseWidget(publicAPI, model) { publicAPI.getMode = () => model.pwMode; publicAPI.shiftPosition = (coords, meta) => { - model.opacityPointShift = - meta.originalOpacityPointShift + coords[0] - meta.originalXY[0]; + if (model.shiftOpacityValues) { + model.opacityValueShift = + meta.originalOpacityValueShift + coords[1] - meta.originalXY[1]; + } else { + model.opacityPointShift = + meta.originalOpacityPointShift + coords[0] - meta.originalXY[0]; + } return true; }; @@ -122,6 +137,7 @@ function vtkPiecewiseWidget(publicAPI, model) { model.dragAction = { originalXY: mouseCoords, originalOpacityPointShift: model.opacityPointShift, + originalOpacityValueShift: model.opacityValueShift, }; return true; @@ -140,7 +156,8 @@ function vtkPiecewiseWidget(publicAPI, model) { if (publicAPI.shiftPosition(normCoords, model.dragAction)) { model.opacities = samplePiecewiseLinear( model.opacityPoints, - model.opacityPointShift + model.opacityPointShift, + model.opacityValueShift ); publicAPI.invokeOpacityChange(publicAPI, true); } @@ -149,22 +166,27 @@ function vtkPiecewiseWidget(publicAPI, model) { return true; }; - publicAPI.setOpacityPoints = (points, shift = 0) => { + publicAPI.setOpacityPoints = (points, shift = 0, shiftAlpha = 0) => { if (publicAPI.isModePoints()) { // deep copy model.opacityPoints = points.map((p) => [p[0], p[1]]); model.opacityPointShift = shift; + model.opacityValueShift = shiftAlpha; model.opacities = samplePiecewiseLinear( model.opacityPoints, - model.opacityPointShift + model.opacityPointShift, + model.opacityValueShift ); publicAPI.modified(); } }; publicAPI.getEffectiveOpacityPoints = () => - model.opacityPoints.map((p) => [p[0] + model.opacityPointShift, p[1]]); + model.opacityPoints.map((p) => [ + p[0] + model.opacityPointShift, + p[1] + model.opacityValueShift, + ]); publicAPI.render = () => { if (publicAPI.isModePoints()) { @@ -188,7 +210,12 @@ export function extend(publicAPI, model, initialValues = {}) { vtkPiecewiseGaussianWidget.extend(publicAPI, model, initialValues); - macro.setGet(publicAPI, model, ['opacityPoints', 'opacityPointShift']); + macro.setGet(publicAPI, model, [ + 'opacityPoints', + 'opacityPointShift', + 'opacityValueShift', + 'shiftOpacityValues', + ]); // Object specific methods vtkPiecewiseWidget(publicAPI, model);