Skip to content

Commit

Permalink
Merge pull request #175 from PaulHax/set-opacity
Browse files Browse the repository at this point in the history
feat(viewer): add opacityPoints event to image actor
  • Loading branch information
thewtex authored Aug 28, 2024
2 parents e433159 + 41362e3 commit 11f916c
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 39 deletions.
8 changes: 8 additions & 0 deletions .changeset/neat-kids-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@itk-viewer/transfer-function-editor': patch
'@itk-viewer/element': patch
'@itk-viewer/viewer': patch
'@itk-viewer/vtkjs': patch
---

Add opacityPoints event to image actor
7 changes: 7 additions & 0 deletions packages/element/examples/view-2d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,12 @@ document.addEventListener('DOMContentLoaded', async function () {
.getSnapshot()
.context.viewports[0].getSnapshot()
.context.views[0].getSnapshot().context.imageActor;

imageActor.send({ type: 'colorMap', colorMap: '2hot', component: 0 });

imageActor.send({
type: 'colorRange',
range: [300, 1500],
component: 0,
});
});
16 changes: 16 additions & 0 deletions packages/element/examples/view-3d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,21 @@ document.addEventListener('DOMContentLoaded', async function () {
.getSnapshot()
.context.viewports[0].getSnapshot()
.context.views[0].getSnapshot().context.imageActor;

imageActor.send({ type: 'colorMap', colorMap: '2hot', component: 0 });

imageActor.send({
type: 'opacityPoints',
points: [
[60, 0.1],
[100, 0.9],
[220, 0.2],
],
component: 0,
});
imageActor.send({
type: 'colorRange',
range: [60, 220],
component: 0,
});
});
6 changes: 1 addition & 5 deletions packages/transfer-function-editor/examples/vtk-js/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
TransferFunctionEditor,
getNodes,
} from '../../lib/TransferFunctionEditor';
import { Point } from '../../lib/Point';

const UPDATE_TF_DELAY = 50;
const DATA_RANGE = [0, 255];
Expand Down Expand Up @@ -33,10 +32,7 @@ if (editorHome) {
editor.eventTarget.addEventListener(
'updated',
throttle((e) => {
const points = (<CustomEvent>e).detail as Point[];
const arrayPoints = points.map((p) => [p.x, p.y]) as Array<
[number, number]
>;
const arrayPoints = (<CustomEvent>e).detail as Array<[number, number]>;
const nodes = getNodes(DATA_RANGE, arrayPoints);
opacityFunction.setNodes(nodes);

Expand Down
24 changes: 14 additions & 10 deletions packages/transfer-function-editor/lib/PiecewiseUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { extendPoints } from './Points';

type ReadOnlyPoints = Array<readonly [number, number]>;

export type ChartStyle = {
lineWidth: number;
strokeStyle: string;
Expand Down Expand Up @@ -101,11 +103,16 @@ export const updateColorCanvas = (
return workCanvas;
};

export const windowPointsForSort = (points: [number, number][]) => {
export const windowPointsForSort = (points: ReadOnlyPoints) => {
const windowedPoints = extendPoints(points);
// avoid unstable Array.sort issues
windowedPoints[0][0] -= 1e-8;
windowedPoints[windowedPoints.length - 1][0] += 1e-8;
const firstPoint = windowedPoints[0];
windowedPoints[0] = [firstPoint[0] - 1e-8, firstPoint[1]];
const lastPoint = windowedPoints[windowedPoints.length - 1];
windowedPoints[windowedPoints.length - 1] = [
lastPoint[0] + 1e-8,
lastPoint[1],
];
return windowedPoints;
};

Expand Down Expand Up @@ -135,9 +142,7 @@ export function rgbaToHexa(rgba: Array<number>) {
return `#${hexa.join('')}`;
}

const findY1Intercepts = (
points: Array<readonly [number, number]>,
): number[] => {
const findY1Intercepts = (points: ReadOnlyPoints): number[] => {
const xPositions: number[] = [];

for (let i = 0; i < points.length - 1; i++) {
Expand All @@ -162,11 +167,10 @@ const findY1Intercepts = (

// Returns vtk.js piecewise function nodes
// rescaled into data range space.
export const getNodes = (
range: readonly number[],
points: Array<readonly [number, number]>,
) => {
export const getNodes = (range: readonly number[], points: ReadOnlyPoints) => {
const xPositions = findY1Intercepts(points);
// vtk.js volume voxels get 0 opacity if y > 1
// So insert y=1 intercept points
const withIntercepts = [...points, ...xPositions.map((x) => [x, 1])];

const windowedPoints = windowPointsForSort(
Expand Down
5 changes: 4 additions & 1 deletion packages/transfer-function-editor/lib/Points.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { ArrayPoint } from './utils';

// if clampEnds is true, add points at ends with y = 0
// else extend left and right points to 0 and 1 with left/right y value
export const extendPoints = (points: [number, number][], clampEnds = false) => {
export const extendPoints = (
points: (readonly [number, number])[],
clampEnds = false,
) => {
if (points.length === 0) {
return [
[0, 1],
Expand Down
85 changes: 66 additions & 19 deletions packages/viewer/src/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,24 @@ export const image = setup({
events:
| { type: 'getWorker'; receiver: AnyActorRef }
| { type: 'builtImage'; builtImage: BuiltImage }
| {
type: 'colorRange';
range: readonly [number, number];
component: number;
}
| {
type: 'normalizedColorRange';
range: readonly [number, number];
component: number;
}
| {
type: 'opacityPoints';
points: Point[];
component: number;
}
| {
type: 'normalizedOpacityPoints';
points: [number, number][];
points: Point[];
component: number;
}
| { type: 'colorMap'; colorMap: string; component: number };
Expand All @@ -90,6 +100,16 @@ export const image = setup({
});
},
}),
updateNormalizedColorRanges: assign({
normalizedColorRanges: ({ context }) => {
return context.dataRanges.map((dataRange, component) => {
return computeNormalizedColorRange(
dataRange,
context.colorRanges[component],
);
});
},
}),
updateOpacityPoints: assign({
opacityPoints: ({ context: { dataRanges, normalizedOpacityPoints } }) => {
return dataRanges.map((range, component) => {
Expand All @@ -100,6 +120,16 @@ export const image = setup({
});
},
}),
updateNormalizedOpacityPoints: assign({
normalizedOpacityPoints: ({ context }) => {
return context.dataRanges.map((dataRange, component) => {
return computeNormalizedOpacityPoints(
dataRange,
context.opacityPoints[component],
);
});
},
}),
ensureComponentDefaults: assign({
colorMaps: ({ context: { dataRanges, colorMaps } }) => {
const components = Array.from(
Expand Down Expand Up @@ -151,36 +181,42 @@ export const image = setup({
},
}),
assign({
normalizedColorRanges: ({ context }) => {
colorRanges: ({ context }) => {
// init color ranges if not set
return context.dataRanges.map((dataRange, component) => {
if (!context.normalizedColorRanges[component])
return NORMALIZED_RANGE_DEFAULT;
// if data range changes
// scale normalizedColorRange so colorRanges doesn't change
const colorRange = context.colorRanges[component];
return computeNormalizedColorRange(dataRange, colorRange);
if (context.colorRanges[component])
return context.colorRanges[component];
return computeColorRange(dataRange, NORMALIZED_RANGE_DEFAULT);
});
},
normalizedOpacityPoints: ({ context }) => {
opacityPoints: ({ context }) => {
// init opacity points if not set
return context.dataRanges.map((dataRange, component) => {
if (!context.normalizedOpacityPoints[component])
return NORMALIZED_OPACITY_POINTS_DEFAULT;
// if data range changes
// scale normalizedPoints so opacityPoints doesn't change
const points = context.opacityPoints[component];
const normalized = computeNormalizedOpacityPoints(
if (context.opacityPoints[component])
return context.opacityPoints[component];
return computeOpacityPoints(
dataRange,
points,
NORMALIZED_OPACITY_POINTS_DEFAULT,
);
return normalized;
});
},
}),
'updateColorRanges',
'updateOpacityPoints',
'updateNormalizedColorRanges',
'updateNormalizedOpacityPoints',
'ensureComponentDefaults',
],
},
colorRange: {
actions: [
assign({
colorRanges: ({ context, event }) => {
context.colorRanges[event.component] = event.range;
return [...context.colorRanges];
},
}),
'updateNormalizedColorRanges',
],
},
normalizedColorRange: {
actions: [
assign({
Expand All @@ -192,6 +228,17 @@ export const image = setup({
'updateColorRanges',
],
},
opacityPoints: {
actions: [
assign({
opacityPoints: ({ context, event }) => {
context.opacityPoints[event.component] = event.points;
return [...context.opacityPoints];
},
}),
'updateNormalizedOpacityPoints',
],
},
normalizedOpacityPoints: {
actions: [
assign({
Expand Down
14 changes: 10 additions & 4 deletions packages/vtkjs/src/view-3d-vtkjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,7 @@ const createImplementation = () => {
) => {
if (!actor) return; // setContainer not called yet. Thats OK because setContainer event will trigger imageSnapshot.
const actorProperty = actor.getProperty();
const { colorRanges, colorMaps, normalizedOpacityPoints, dataRanges } =
state.context;
const { colorRanges, colorMaps, opacityPoints } = state.context;

colorMaps.forEach((colorMap, component) => {
const colorFunc = actorProperty.getRGBTransferFunction(component);
Expand All @@ -216,9 +215,16 @@ const createImplementation = () => {
ct.setMappingRange(...range);
});

normalizedOpacityPoints.forEach((points, component) => {
opacityPoints.forEach((points, component) => {
const xValues = points.map((p) => p[0]);
const range = [Math.min(...xValues), Math.max(...xValues)];
const delta = range[1] - range[0];
const min = range[0];
const normalizedPoints = points.map(
([x, y]) => [(x - min) / delta, y] as const,
);
const nodes = getNodes(range, normalizedPoints);
const opacityFunc = actorProperty.getScalarOpacity(component);
const nodes = getNodes(dataRanges[component], points);
opacityFunc.setNodes(nodes);
});

Expand Down

0 comments on commit 11f916c

Please sign in to comment.