import macro from 'vtk.js/Sources/macros'; import vtkAbstractWidgetFactory from 'vtk.js/Sources/Widgets/Core/AbstractWidgetFactory'; import vtkBoundingBox from 'vtk.js/Sources/Common/DataModel/BoundingBox'; import vtkBox from 'vtk.js/Sources/Common/DataModel/Box'; import vtkPlane from 'vtk.js/Sources/Common/DataModel/Plane'; import vtkPlaneSource from 'vtk.js/Sources/Filters/Sources/PlaneSource'; import vtkPlaneManipulator from 'vtk.js/Sources/Widgets/Manipulators/PlaneManipulator'; import vtkLineHandleRepresentation from 'vtk.js/Sources/Widgets/Representations/LineHandleRepresentation'; import vtkSphereHandleRepresentation from 'vtk.js/Sources/Widgets/Representations/SphereHandleRepresentation';
import * as vtkMath from 'vtk.js/Sources/Common/Core/Math';
import widgetBehavior from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget/behavior'; import stateGenerator from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget/state'; import { boundPlane, updateState, transformPlane, } from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget/helpers'; import { viewTypeToPlaneName } from 'vtk.js/Sources/Widgets/Widgets3D/ResliceCursorWidget/Constants';
import { ViewTypes } from 'vtk.js/Sources/Widgets/Core/WidgetManager/Constants';
import { mat4 } from 'gl-matrix'; import vtkMatrixBuilder from 'vtk.js/Sources/Common/Core/MatrixBuilder';
const VTK_INT_MAX = 2147483647; const { vtkErrorMacro } = macro;
function vtkResliceCursorWidget(publicAPI, model) { model.classHierarchy.push('vtkResliceCursorWidget');
model.methodsToLink = [ 'scaleInPixels', 'holeWidth', 'infiniteLine', ];
function computeReslicePlaneOrigin(viewType) { const bounds = model.widgetState.getImage().getBounds();
const center = publicAPI.getWidgetState().getCenter(); const imageCenter = model.widgetState.getImage().getCenter();
const offset = []; for (let i = 0; i < 3; i++) { offset[i] = -Math.abs(center[i] - imageCenter[i]); offset[i] *= 2; }
const planeSource = vtkPlaneSource.newInstance();
if (viewType === ViewTypes.XZ_PLANE) { planeSource.setOrigin( bounds[0] + offset[0], center[1], bounds[4] + offset[2] ); planeSource.setPoint1( bounds[1] - offset[0], center[1], bounds[4] + offset[2] ); planeSource.setPoint2( bounds[0] + offset[0], center[1], bounds[5] - offset[2] ); } else if (viewType === ViewTypes.XY_PLANE) { planeSource.setOrigin( bounds[0] + offset[0], bounds[2] + offset[1], center[2] ); planeSource.setPoint1( bounds[1] - offset[0], bounds[2] + offset[1], center[2] ); planeSource.setPoint2( bounds[0] + offset[0], bounds[3] - offset[1], center[2] ); } else if (viewType === ViewTypes.YZ_PLANE) { planeSource.setOrigin( center[0], bounds[2] + offset[1], bounds[4] + offset[2] ); planeSource.setPoint1( center[0], bounds[3] - offset[1], bounds[4] + offset[2] ); planeSource.setPoint2( center[0], bounds[2] + offset[1], bounds[5] - offset[2] ); } return planeSource; }
function computeFocalPointOffsetFromResliceCursorCenter(viewType, renderer) { const worldFocalPoint = renderer.getActiveCamera().getFocalPoint(); const worldResliceCenter = model.widgetState.getCenter();
const view = renderer.getRenderWindow().getViews()[0]; const dims = view.getViewportSize(renderer); const aspect = dims[0] / dims[1]; const displayFocalPoint = renderer.worldToNormalizedDisplay( ...worldFocalPoint, aspect ); const displayResliceCenter = renderer.worldToNormalizedDisplay( ...worldResliceCenter, aspect );
const newOffset = vtkMath.subtract( displayFocalPoint, displayResliceCenter, [0, 0, 0] );
const cameraOffsets = model.widgetState.getCameraOffsets(); cameraOffsets[viewType] = newOffset;
model.widgetState.setCameraOffsets(cameraOffsets); }
function updateCamera( renderer, normal, viewType, resetFocalPoint, keepCenterFocalDistance ) {
const focalPoint = renderer.getActiveCamera().getFocalPoint();
const distance = renderer.getActiveCamera().getDistance();
const estimatedCameraPosition = vtkMath.multiplyAccumulate( focalPoint, normal, distance, [0, 0, 0] );
let newFocalPoint = focalPoint; if (resetFocalPoint) { const intersection = vtkPlane.intersectWithLine( focalPoint, estimatedCameraPosition, model.widgetState.getCenter(), normal ); newFocalPoint = intersection.x; }
if (keepCenterFocalDistance) { const worldResliceCenter = model.widgetState.getCenter();
const view = renderer.getRenderWindow().getViews()[0]; const dims = view.getViewportSize(renderer); const aspect = dims[0] / dims[1]; const displayResliceCenter = renderer.worldToNormalizedDisplay( ...worldResliceCenter, aspect );
const realOffset = model.widgetState.getCameraOffsets()[viewType]; const displayFocal = vtkMath.add( displayResliceCenter, realOffset, [0, 0, 0] );
const worldFocal = renderer.normalizedDisplayToWorld( ...displayFocal, aspect );
const intersection2 = vtkPlane.intersectWithLine( worldFocal, estimatedCameraPosition, worldResliceCenter, normal );
newFocalPoint[0] = intersection2.x[0]; newFocalPoint[1] = intersection2.x[1]; newFocalPoint[2] = intersection2.x[2]; }
renderer .getActiveCamera() .setFocalPoint(newFocalPoint[0], newFocalPoint[1], newFocalPoint[2]);
const newCameraPosition = vtkMath.multiplyAccumulate( newFocalPoint, normal, distance, [0, 0, 0] );
renderer .getActiveCamera() .setPosition( newCameraPosition[0], newCameraPosition[1], newCameraPosition[2] );
const bounds = model.widgetState.getImage().getBounds(); if (resetFocalPoint) { renderer.resetCamera(bounds); } renderer.resetCameraClippingRange(bounds); }
function findWidgetForViewType(viewType) { return publicAPI .getViewIds() .map((viewId) => publicAPI.getWidgetForView({ viewId })) .find((widget) => widget.getViewType() === viewType); }
function findRepresentationsForViewType(viewType) { const widgetForViewType = findWidgetForViewType(viewType); return widgetForViewType ? widgetForViewType.getRepresentations() : []; }
publicAPI.getRepresentationsForViewType = (viewType) => { switch (viewType) { case ViewTypes.XY_PLANE: case ViewTypes.XZ_PLANE: case ViewTypes.YZ_PLANE: return [ { builder: vtkLineHandleRepresentation, labels: [`lineIn${viewTypeToPlaneName[viewType]}`], initialValues: { useActiveColor: false, scaleInPixels: model.scaleInPixels, }, }, { builder: vtkSphereHandleRepresentation, labels: [`rotationIn${viewTypeToPlaneName[viewType]}`], initialValues: { useActiveColor: false, scaleInPixels: model.scaleInPixels, lighting: false, }, }, { builder: vtkSphereHandleRepresentation, labels: ['center'], initialValues: { useActiveColor: false, scaleInPixels: model.scaleInPixels, lighting: false, }, }, ]; case ViewTypes.DEFAULT: case ViewTypes.GEOMETRY: case ViewTypes.SLICE: case ViewTypes.VOLUME: default: return []; } };
publicAPI.setImage = (image) => { model.widgetState.setImage(image); const center = image.getCenter(); publicAPI.setCenter(center); };
publicAPI.setCenter = (center) => { model.widgetState.setCenter(center); updateState( model.widgetState, model.scaleInPixels, model.rotationHandlePosition ); publicAPI.modified(); };
publicAPI.updateCameraPoints = ( renderer, viewType, resetFocalPoint, computeFocalPointOffset ) => { publicAPI.resetCamera( renderer, viewType, resetFocalPoint, !computeFocalPointOffset );
if (computeFocalPointOffset) { computeFocalPointOffsetFromResliceCursorCenter(viewType, renderer); } };
publicAPI.resetCamera = ( renderer, viewType, resetFocalPoint, keepCenterFocalDistance ) => { const center = model.widgetState.getImage().getCenter(); const focalPoint = renderer.getActiveCamera().getFocalPoint(); const position = renderer.getActiveCamera().getPosition();
const distance = Math.sqrt( vtkMath.distance2BetweenPoints(position, focalPoint) );
const normal = publicAPI.getPlaneNormalFromViewType(viewType);
const estimatedFocalPoint = resetFocalPoint ? center : focalPoint;
const estimatedCameraPosition = vtkMath.multiplyAccumulate( estimatedFocalPoint, normal, distance, [0, 0, 0] ); renderer.getActiveCamera().setFocalPoint(...estimatedFocalPoint); renderer.getActiveCamera().setPosition(...estimatedCameraPosition); renderer .getActiveCamera() .setViewUp(model.widgetState.getPlanes()[viewType].viewUp);
updateCamera( renderer, normal, viewType, resetFocalPoint, keepCenterFocalDistance ); };
publicAPI.getPlaneSource = (viewType) => { const planeSource = computeReslicePlaneOrigin(viewType);
const { normal, viewUp } = model.widgetState.getPlanes()[viewType]; transformPlane(planeSource, model.widgetState.getCenter(), normal, viewUp);
const boundedOrigin = [...planeSource.getOrigin()]; const boundedP1 = [...planeSource.getPoint1()]; const boundedP2 = [...planeSource.getPoint2()];
boundPlane( model.widgetState.getImage().getBounds(), boundedOrigin, boundedP1, boundedP2 );
planeSource.setOrigin(...boundedOrigin); planeSource.setPoint1(...boundedP1); planeSource.setPoint2(...boundedP2);
return planeSource; };
publicAPI.getResliceAxes = (viewType) => { const planeSource = publicAPI.getPlaneSource(viewType);
const { normal } = model.widgetState.getPlanes()[viewType];
const planeOrigin = planeSource.getOrigin();
const p1 = planeSource.getPoint1(); const planeAxis1 = []; vtkMath.subtract(p1, planeOrigin, planeAxis1); vtkMath.normalize(planeAxis1);
const p2 = planeSource.getPoint2(); const planeAxis2 = []; vtkMath.subtract(p2, planeOrigin, planeAxis2); vtkMath.normalize(planeAxis2);
const newResliceAxes = mat4.identity(new Float64Array(16));
for (let i = 0; i < 3; i++) { newResliceAxes[i] = planeAxis1[i]; newResliceAxes[4 + i] = planeAxis2[i]; newResliceAxes[8 + i] = normal[i]; newResliceAxes[12 + i] = planeOrigin[i]; }
return newResliceAxes; };
publicAPI.updateReslicePlane = (imageReslice, viewType) => { const spacing = model.widgetState.getImage().getSpacing();
const planeSource = publicAPI.getPlaneSource(viewType); const newResliceAxes = publicAPI.getResliceAxes(viewType);
const planeOrigin = planeSource.getOrigin(); const p1 = planeSource.getPoint1(); const planeAxis1 = vtkMath.subtract(p1, planeOrigin, []); const planeSizeX = vtkMath.normalize(planeAxis1);
const p2 = planeSource.getPoint2(); const planeAxis2 = vtkMath.subtract(p2, planeOrigin, []); const planeSizeY = vtkMath.normalize(planeAxis2);
const spacingX = Math.abs(planeAxis1[0] * spacing[0]) + Math.abs(planeAxis1[1] * spacing[1]) + Math.abs(planeAxis1[2] * spacing[2]);
const spacingY = Math.abs(planeAxis2[0] * spacing[0]) + Math.abs(planeAxis2[1] * spacing[1]) + Math.abs(planeAxis2[2] * spacing[2]);
let extentX = 0; let extentY = 0;
const realExtentX = spacingX === 0 ? Number.MAX_SAFE_INTEGER : planeSizeX / spacingX;
const value = VTK_INT_MAX >> 1;
if (realExtentX > value) { vtkErrorMacro( 'Invalid X extent: ', realExtentX, ' on view type : ', viewType ); extentX = 0; } else { extentX = 1; while (extentX < realExtentX) { extentX <<= 1; } }
const realExtentY = spacingY === 0 ? Number.MAX_SAFE_INTEGER : planeSizeY / spacingY;
if (realExtentY > value) { vtkErrorMacro( 'Invalid Y extent:', realExtentY, ' on view type : ', viewType ); extentY = 0; } else { extentY = 1; while (extentY < realExtentY) { extentY <<= 1; } }
const outputSpacingX = extentX === 0 ? 1.0 : planeSizeX / extentX; const outputSpacingY = extentY === 0 ? 1.0 : planeSizeY / extentY;
let modified = imageReslice.setResliceAxes(newResliceAxes); modified = imageReslice.setOutputSpacing([outputSpacingX, outputSpacingY, 1]) || modified; modified = imageReslice.setOutputOrigin([ 0.5 * outputSpacingX, 0.5 * outputSpacingY, 0, ]) || modified; modified = imageReslice.setOutputExtent([0, extentX - 1, 0, extentY - 1, 0, 0]) || modified;
return modified; };
publicAPI.getPlaneSourceFromViewType = (type) => { const planeSource = vtkPlaneSource.newInstance(); const origin = publicAPI.getWidgetState().getCenter(); const planeNormal = publicAPI.getPlaneNormalFromViewType(type);
planeSource.setNormal(planeNormal); planeSource.setOrigin(origin);
return planeSource; };
publicAPI.getPlaneNormalFromViewType = (viewType) => publicAPI.getWidgetState().getPlanes()[viewType].normal;
publicAPI.getOtherPlaneNormals = (viewType) => [ViewTypes.YZ_PLANE, ViewTypes.XZ_PLANE, ViewTypes.XY_PLANE] .filter((vt) => vt !== viewType) .map((vt) => publicAPI.getPlaneNormalFromViewType(vt));
publicAPI.getResliceMatrix = () => { const resliceMatrix = mat4.identity(new Float64Array(16));
for (let i = 0; i < 3; i++) { resliceMatrix[4 * i + 0] = publicAPI.getPlaneNormalFromViewType( ViewTypes.YZ_PLANE )[i]; resliceMatrix[4 * i + 1] = publicAPI.getPlaneNormalFromViewType( ViewTypes.XZ_PLANE )[i]; resliceMatrix[4 * i + 2] = publicAPI.getPlaneNormalFromViewType( ViewTypes.XY_PLANE )[i]; }
const origin = publicAPI.getWidgetState().getCenter();
const m = vtkMatrixBuilder .buildFromRadian() .translate(...origin) .multiply(resliceMatrix) .translate(...vtkMath.multiplyScalar([...origin], -1)) .getMatrix();
return m; };
publicAPI.getDisplayScaleParams = () => [ViewTypes.YZ_PLANE, ViewTypes.XZ_PLANE, ViewTypes.XY_PLANE].reduce( (res, viewType) => { res[viewType] = findRepresentationsForViewType( viewType )[0]?.getDisplayScaleParams?.(); return res; }, {} ); publicAPI.setScaleInPixels = macro.chain( publicAPI.setScaleInPixels, (scale) => { publicAPI.getViewWidgets().forEach((w) => w.setScaleInPixels(scale)); updateState( model.widgetState, model.scaleInPixels, model.rotationHandlePosition ); } );
publicAPI.getPlaneExtremities = (viewType) => { const dirProj = publicAPI.getWidgetState().getPlanes()[viewType].normal; const length = vtkBoundingBox.getDiagonalLength( publicAPI.getWidgetState().getImage().getBounds() ); const p1 = vtkMath.multiplyAccumulate( publicAPI.getWidgetState().getCenter(), dirProj, -length, [] ); const p2 = vtkMath.multiplyAccumulate( publicAPI.getWidgetState().getCenter(), dirProj, length, [] ); const intersectionPoints = vtkBox.intersectWithLine( publicAPI.getWidgetState().getImage().getBounds(), p1, p2 ); return [intersectionPoints.x1, intersectionPoints.x2]; }; }
const defaultValues = (initialValues) => ({ behavior: widgetBehavior, widgetState: stateGenerator(initialValues.planes), rotationHandlePosition: 0.5, scaleInPixels: true, manipulator: vtkPlaneManipulator.newInstance(), ...initialValues, });
export function extend(publicAPI, model, initialValues = {}) { Object.assign(model, defaultValues(initialValues));
vtkAbstractWidgetFactory.extend(publicAPI, model, initialValues);
macro.setGet(publicAPI, model, [ 'scaleInPixels', 'rotationHandlePosition', 'manipulator', ]);
vtkResliceCursorWidget(publicAPI, model); }
export const newInstance = macro.newInstance(extend, 'vtkResliceCursorWidget');
export default { newInstance, extend };
|