Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 | 128x | import { vec3 } from 'gl-matrix';
import cache from '../../../cache/cache';
import { OrientationAxis } from '../../../enums';
import type { RenderBackendValue } from '../../../enums';
import { getEffectiveRenderBackend } from '../../../init';
import * as metaData from '../../../metaData';
import { getRenderModeForBackend } from '../../helpers/renderBackendRegistry';
import type { EffectiveRenderBackend } from '../../../types/RenderBackendRegistry';
import { isValidVolume } from '../../../utilities/isValidVolume';
import { getOrientationFromScanAxisNormal } from '../../helpers/getCameraVectors';
import type {
PlanarViewState,
PlanarEffectiveRenderMode,
PlanarOrientation,
PlanarRegisteredDataSet,
} from './PlanarViewportTypes';
export interface SelectedPlanarRenderPath {
acquisitionOrientation?: PlanarViewState['orientation'];
renderMode: PlanarEffectiveRenderMode;
volumeId: string;
}
export interface PlanarRenderPathDecisionOptions {
orientation?: PlanarOrientation;
useSliceRendering?: boolean;
/**
* Per-mount render backend override. 'cpu' and 'gpu' pin this dataset to
* that backend; 'auto' resolves from the capability detection regardless of
* the global pin. When omitted, the global
* rendering.planar.renderBackend configuration decides.
*/
renderBackend?: RenderBackendValue;
}
/**
* Centralizes PlanarViewport render-path decisions.
*
* Clean Planar Next callers do not pass a render mode. This service derives the
* internal path from semantic inputs: dataset shape, requested orientation,
* and the render backend (per-mount override or global
* rendering.planar.renderBackend configuration, with 'auto' resolved from
* init-time capability detection). Binding role is deliberately not part of
* the decision; source and overlays are mounted through the same rules.
*/
export class PlanarRenderPathDecisionService {
select(
dataSet: PlanarRegisteredDataSet,
options: PlanarRenderPathDecisionOptions = {}
): SelectedPlanarRenderPath {
if (!dataSet.imageIds.length) {
throw new Error('[PlanarViewport] Cannot add an empty planar dataset');
}
const orientation = options.orientation || OrientationAxis.ACQUISITION;
const acquisitionOrientation = getPlanarAcquisitionOrientation(
dataSet.imageIds
);
const volumeId = getVolumeId(dataSet);
const isAcquisitionPath =
orientation === OrientationAxis.ACQUISITION ||
(acquisitionOrientation !== undefined &&
orientation === acquisitionOrientation);
const useVolumePath = isVolumeBackedDataSet(dataSet, isAcquisitionPath);
if (
!isAcquisitionPath &&
!useVolumePath &&
!dataSet.useWorldCoordinateImageData
) {
throw new Error(
'[PlanarViewport] Non-acquisition rendering requires a valid volume dataset'
);
}
if (useVolumePath && !supportsVolumeRendering(dataSet)) {
throw new Error(
'[PlanarViewport] Volume rendering requires a valid volume dataset'
);
}
return {
acquisitionOrientation,
renderMode: useVolumePath
? this.selectVolumeRenderMode(options)
: this.selectImageRenderMode(options),
volumeId,
};
}
/**
* Non-throwing companion to {@link select}: reports whether the dataset can be
* rendered in the requested orientation/configuration, without selecting a
* render mode. Lets callers (e.g. OHIF MPR/orientation controls) pre-check an
* orientation before requesting it, instead of relying on `select()` throwing.
*
* Keep the renderability rules here in sync with `select()` above.
*/
canRender(
dataSet: PlanarRegisteredDataSet,
options: PlanarRenderPathDecisionOptions = {}
): boolean {
if (!dataSet.imageIds.length) {
return false;
}
const orientation = options.orientation || OrientationAxis.ACQUISITION;
const acquisitionOrientation = getPlanarAcquisitionOrientation(
dataSet.imageIds
);
const isAcquisitionPath =
orientation === OrientationAxis.ACQUISITION ||
(acquisitionOrientation !== undefined &&
orientation === acquisitionOrientation);
const useVolumePath = isVolumeBackedDataSet(dataSet, isAcquisitionPath);
if (
!isAcquisitionPath &&
!useVolumePath &&
!dataSet.useWorldCoordinateImageData
) {
return false;
}
if (useVolumePath && !supportsVolumeRendering(dataSet)) {
return false;
}
return true;
}
private selectImageRenderMode(
options: PlanarRenderPathDecisionOptions
): PlanarEffectiveRenderMode {
return getRenderModeForBackend(this.resolveBackend(options), 'image');
}
private selectVolumeRenderMode(
options: PlanarRenderPathDecisionOptions
): PlanarEffectiveRenderMode {
return getRenderModeForBackend(this.resolveBackend(options), 'volume');
}
/**
* Resolves the effective backend for one decision: the per-mount override
* when present ('auto' resolves from capability detection even when the
* global backend is pinned), the global configuration otherwise. The
* precedence ladder itself lives in getEffectiveRenderBackend so it cannot
* drift from the global resolution. The backend's render modes come from
* its registerRenderBackend() definition.
*/
private resolveBackend(
options: PlanarRenderPathDecisionOptions
): EffectiveRenderBackend {
return getEffectiveRenderBackend(options.renderBackend);
}
}
export const defaultPlanarRenderPathDecisionService =
new PlanarRenderPathDecisionService();
export function selectPlanarRenderPath(
dataSet: PlanarRegisteredDataSet,
options: PlanarRenderPathDecisionOptions = {}
): SelectedPlanarRenderPath {
return defaultPlanarRenderPathDecisionService.select(dataSet, options);
}
export function getPlanarAcquisitionOrientation(
imageIds: string[]
): PlanarViewState['orientation'] | undefined {
const imageId = imageIds[0];
if (!imageId) {
return;
}
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
const imageOrientationPatient = imagePlaneModule?.imageOrientationPatient;
if (!imageOrientationPatient || imageOrientationPatient.length !== 6) {
return;
}
const rowVec = vec3.fromValues(
imageOrientationPatient[0],
imageOrientationPatient[1],
imageOrientationPatient[2]
);
const colVec = vec3.fromValues(
imageOrientationPatient[3],
imageOrientationPatient[4],
imageOrientationPatient[5]
);
const scanAxisNormal = vec3.create();
vec3.cross(scanAxisNormal, rowVec, colVec);
const orientation = getOrientationFromScanAxisNormal([
scanAxisNormal[0],
scanAxisNormal[1],
scanAxisNormal[2],
]);
if (
orientation !== OrientationAxis.AXIAL &&
orientation !== OrientationAxis.CORONAL &&
orientation !== OrientationAxis.SAGITTAL
) {
return;
}
return orientation;
}
function getVolumeId(dataSet: PlanarRegisteredDataSet): string {
return dataSet.volumeId || cache.generateVolumeId(dataSet.imageIds);
}
function hasExplicitCachedVolume(dataSet: PlanarRegisteredDataSet): boolean {
return !!(dataSet.volumeId && cache.getVolume(dataSet.volumeId));
}
function supportsVolumeRendering(dataSet: PlanarRegisteredDataSet): boolean {
return hasExplicitCachedVolume(dataSet) || isValidVolume(dataSet.imageIds);
}
function isVolumeBackedDataSet(
dataSet: PlanarRegisteredDataSet,
isAcquisitionPath: boolean
): boolean {
if (dataSet.useWorldCoordinateImageData) {
return false;
}
if (dataSet.volumeId) {
return true;
}
return !isAcquisitionPath && supportsVolumeRendering(dataSet);
}
|