UNPKG

@kitware/vtk.js

Version:

Visualization Toolkit for the Web

354 lines (328 loc) 13.2 kB
import { m as macro } from '../../macros2.js'; import vtkMouseRangeManipulator from '../../Interaction/Manipulators/MouseRangeManipulator.js'; import vtkViewProxy from './ViewProxy.js'; import { j as cross, H as getMajorAxisIndex, r as radiansFromDegrees } from '../../Common/Core/Math/index.js'; import { mat4, vec3 } from 'gl-matrix'; import vtkBoundingBox from '../../Common/DataModel/BoundingBox.js'; const DEFAULT_STEP_WIDTH = 512; function formatAnnotationValue(value) { if (Array.isArray(value)) { return value.map(formatAnnotationValue).join(', '); } if (Number.isInteger(value)) { return value; } if (Number.isFinite(value)) { if (Math.abs(value) < 0.01) { return '0'; } return value.toFixed(2); } return value; } /** * Returns an array of points in world coordinates creating a coarse hull * around the prop given in argument * The returned array is empty if the prop is not visible or doesn't use bounds * * How it works: if possible, combine the mapper bounds corners with the prop matrix * otherwise, returns the prop bounds corners */ function getPropCoarseHull(prop) { if (!prop.getVisibility() || !prop.getUseBounds()) { return []; } let finestBounds = prop.getBounds(); let finestMatrix = null; // Better bounds using mapper bounds and prop matrix const mapper = prop?.getMapper?.(); const mapperBounds = mapper?.getBounds?.(); if (vtkBoundingBox.isValid(mapperBounds) && prop.getMatrix) { finestBounds = mapperBounds; finestMatrix = prop.getMatrix().slice(); mat4.transpose(finestMatrix, finestMatrix); // Better bounds using the image data matrix and prop matrix + imageData matrix if (mapper.isA('vtkImageMapper') && mapper.getInputData()?.isA('vtkImageData')) { prop.computeMatrix(); const imageData = mapper.getInputData(); finestBounds = imageData.getSpatialExtent(); const imageDataMatrix = imageData.getIndexToWorld(); mat4.mul(finestMatrix, finestMatrix, imageDataMatrix); } } // Compute corners and transform them if needed // It gives a more accurate hull than computing the corners of a transformed bounding box if (!vtkBoundingBox.isValid(finestBounds)) { return []; } const corners = []; vtkBoundingBox.getCorners(finestBounds, corners); if (finestMatrix) { corners.forEach(pt => vec3.transformMat4(pt, pt, finestMatrix)); } return corners; } // ---------------------------------------------------------------------------- // vtkView2DProxy methods // ---------------------------------------------------------------------------- function vtkView2DProxy(publicAPI, model) { // Set our className model.classHierarchy.push('vtkView2DProxy'); publicAPI.updateWidthHeightAnnotation = () => { const { ijkOrientation, dimensions } = model.cornerAnnotation.getMetadata(); if (ijkOrientation && dimensions) { let realDimensions = dimensions; if (dimensions.length > 3) { // the dimensions is a string realDimensions = dimensions.split(',').map(Number); } const dop = model.camera.getDirectionOfProjection(); const viewUp = model.camera.getViewUp(); const viewRight = [0, 0, 0]; cross(dop, viewUp, viewRight); const wIdx = getMajorAxisIndex(viewRight); const hIdx = getMajorAxisIndex(viewUp); const sliceWidth = realDimensions['IJK'.indexOf(ijkOrientation[wIdx])]; const sliceHeight = realDimensions['IJK'.indexOf(ijkOrientation[hIdx])]; publicAPI.updateCornerAnnotation({ sliceWidth, sliceHeight }); } }; const superUpdateOrientation = publicAPI.updateOrientation; publicAPI.updateOrientation = (axisIndex, orientation, viewUp) => { const promise = superUpdateOrientation(axisIndex, orientation, viewUp); let count = model.representations.length; while (count--) { const rep = model.representations[count]; const slicingMode = 'XYZ'[axisIndex]; if (rep.setSlicingMode) { rep.setSlicingMode(slicingMode); } } publicAPI.updateCornerAnnotation({ axis: 'XYZ'[axisIndex] }); return promise; }; const superAddRepresentation = publicAPI.addRepresentation; publicAPI.addRepresentation = rep => { superAddRepresentation(rep); if (rep.setSlicingMode) { rep.setSlicingMode('XYZ'[model.axis]); } publicAPI.bindRepresentationToManipulator(rep); }; const superRemoveRepresentation = publicAPI.removeRepresentation; publicAPI.removeRepresentation = rep => { superRemoveRepresentation(rep); if (rep === model.sliceRepresentation) { publicAPI.bindRepresentationToManipulator(null); let count = model.representations.length; while (count--) { if (publicAPI.bindRepresentationToManipulator(model.representations[count])) { count = 0; } } } }; const superInternalResetCamera = model._resetCamera; /** * If fitProps is true, calling resetCamera will exactly fit the bounds in the view * Exact fitting requires useParallelRendering, and an active camera * Otherwise, the default renderer.resetCamera is used and it uses a larger bounding box */ model._resetCamera = (bounds = null) => { // Always reset camera first to set physicalScale, physicalTranslation and trigger events const initialReset = superInternalResetCamera(bounds); if (!model.fitProps || !model.useParallelRendering || !initialReset) { return initialReset; } // For each visible prop get the smallest possible convex hull using bounds corners const visiblePoints = []; if (bounds) { // Bounds are given as argument, use their corners vtkBoundingBox.getCorners(bounds, visiblePoints); } else { publicAPI.getRepresentations().forEach(representationProxy => [representationProxy.getActors(), representationProxy.getVolumes()].flat().forEach(prop => visiblePoints.push(...getPropCoarseHull(prop)))); } if (!visiblePoints) { return initialReset; } // Get the bounds in view coordinates const viewBounds = vtkBoundingBox.reset([]); const viewMatrix = model.camera.getViewMatrix(); mat4.transpose(viewMatrix, viewMatrix); for (let i = 0; i < visiblePoints.length; ++i) { const point = visiblePoints[i]; vec3.transformMat4(point, point, viewMatrix); vtkBoundingBox.addPoint(viewBounds, ...point); } // Compute parallel scale const view = model.renderer.getRenderWindow().getViews()[0]; const dims = view.getViewportSize(model.renderer); const aspect = dims[1] && dims[0] ? dims[0] / dims[1] : 1; const xLength = vtkBoundingBox.getLength(viewBounds, 0); const yLength = vtkBoundingBox.getLength(viewBounds, 1); const parallelScale = 0.5 * Math.max(yLength, xLength / aspect); // Compute focal point and position const viewFocalPoint = vtkBoundingBox.getCenter(viewBounds); // Camera position in view coordinates is the center of the bounds in XY // and is (the maximum bound) + (the distance to see the bounds in perspective) in Z const perspectiveAngle = radiansFromDegrees(model.camera.getViewAngle()); const distance = parallelScale / Math.tan(perspectiveAngle * 0.5); const viewPosition = [viewFocalPoint[0], viewFocalPoint[1], viewBounds[5] + distance]; const inverseViewMatrix = new Float64Array(16); const worldFocalPoint = new Float64Array(3); const worldPosition = new Float64Array(3); mat4.invert(inverseViewMatrix, viewMatrix); vec3.transformMat4(worldFocalPoint, viewFocalPoint, inverseViewMatrix); vec3.transformMat4(worldPosition, viewPosition, inverseViewMatrix); if (parallelScale <= 0) { return initialReset; } // Compute bounds in world coordinates const worldBounds = vtkBoundingBox.transformBounds(viewBounds, inverseViewMatrix); publicAPI.setCameraParameters({ position: worldPosition, focalPoint: worldFocalPoint, bounds: worldBounds, parallelScale }); return true; }; // -------------------------------------------------------------------------- // Range Manipulator setup // ------------------------------------------------------------------------- model.rangeManipulator = vtkMouseRangeManipulator.newInstance({ button: 1, scrollEnabled: true }); model.interactorStyle2D.addMouseManipulator(model.rangeManipulator); function setWindowWidth(windowWidth) { publicAPI.updateCornerAnnotation({ windowWidth }); if (model.sliceRepresentation && model.sliceRepresentation.setWindowWidth) { model.sliceRepresentation.setWindowWidth(windowWidth); } } function setWindowLevel(windowLevel) { publicAPI.updateCornerAnnotation({ windowLevel }); if (model.sliceRepresentation && model.sliceRepresentation.setWindowLevel) { model.sliceRepresentation.setWindowLevel(windowLevel); } } function setSlice(sliceRaw) { const numberSliceRaw = Number(sliceRaw); const slice = Number.isInteger(numberSliceRaw) ? sliceRaw : numberSliceRaw.toFixed(2); // add 'slice' in annotation const annotation = { slice }; if (model.sliceRepresentation && model.sliceRepresentation.setSlice) { model.sliceRepresentation.setSlice(numberSliceRaw); } // extend annotation if (model.sliceRepresentation && model.sliceRepresentation.getAnnotations) { const addOn = model.sliceRepresentation.getAnnotations(); Object.keys(addOn).forEach(key => { annotation[key] = formatAnnotationValue(addOn[key]); }); } publicAPI.updateCornerAnnotation(annotation); } publicAPI.bindRepresentationToManipulator = representation => { let nbListeners = 0; if (!representation.getProxyId) { return nbListeners; } model.rangeManipulator.removeAllListeners(); model.sliceRepresentation = representation; while (model.sliceRepresentationSubscriptions.length) { model.sliceRepresentationSubscriptions.pop().unsubscribe(); } if (representation) { model.sliceRepresentationSubscriptions.push(model.camera.onModified(publicAPI.updateWidthHeightAnnotation)); if (representation.getWindowWidth) { const update = () => setWindowWidth(representation.getWindowWidth()); const windowWidth = representation.getPropertyDomainByName('windowWidth'); const { min, max } = windowWidth; let { step } = windowWidth; if (!step || step === 'any') { step = 1 / DEFAULT_STEP_WIDTH; } model.rangeManipulator.setVerticalListener(min, max, step, representation.getWindowWidth, setWindowWidth); model.sliceRepresentationSubscriptions.push(representation.onModified(update)); update(); nbListeners++; } if (representation.getWindowLevel) { const update = () => setWindowLevel(representation.getWindowLevel()); const windowLevel = representation.getPropertyDomainByName('windowLevel'); const { min, max } = windowLevel; let { step } = windowLevel; if (!step || step === 'any') { step = 1 / DEFAULT_STEP_WIDTH; } model.rangeManipulator.setHorizontalListener(min, max, step, representation.getWindowLevel, setWindowLevel); model.sliceRepresentationSubscriptions.push(representation.onModified(update)); update(); nbListeners++; } const domain = representation.getPropertyDomainByName('slice'); if (representation.getSlice && domain) { const update = () => setSlice(representation.getSlice()); model.rangeManipulator.setScrollListener(domain.min, domain.max, domain.step, representation.getSlice, setSlice); model.sliceRepresentationSubscriptions.push(representation.onModified(update)); update(); nbListeners++; } } return nbListeners; }; } // ---------------------------------------------------------------------------- // Object factory // ---------------------------------------------------------------------------- const DEFAULT_VALUES = { axis: 2, orientation: -1, viewUp: [0, 1, 0], useParallelRendering: true, sliceRepresentationSubscriptions: [], fitProps: false }; // ---------------------------------------------------------------------------- function extend(publicAPI, model, initialValues = {}) { Object.assign(model, DEFAULT_VALUES, initialValues); vtkViewProxy.extend(publicAPI, model, initialValues); macro.get(publicAPI, model, ['axis']); macro.setGet(publicAPI, model, ['fitProps']); // Object specific methods vtkView2DProxy(publicAPI, model); } // ---------------------------------------------------------------------------- const newInstance = macro.newInstance(extend, 'vtkView2DProxy'); // ---------------------------------------------------------------------------- var vtkView2DProxy$1 = { newInstance, extend }; export { vtkView2DProxy$1 as default, extend, newInstance };