Viewport Projection
Viewport Projection is the Generic Viewport construct for asking how a viewport's semantic state maps into presentation, coordinate transforms, and renderer output.
It exists because zoom, scale, pan, and camera fields do not mean the same
thing for every viewport family. A planar viewport has a semantic anchor,
slice geometry, display area, and derived renderer camera. A 3D viewport is
runtime-camera-backed. Video, ECG, and WSI have their own mapping rules. The
shared abstraction is therefore not ICamera; ICamera remains renderer
output when a renderer needs it.
What "projection" means here
In this codebase, "projection" is the mathematical sense — the act of
projecting a viewport's semantic state onto a presentation, a set of
coordinate transforms, and (when applicable) a renderer camera. A Projection
is the adaptation seam between a viewport's internal model and the cross-family
surface that synchronizers and tools consume.
It is not VTK's parallel-vs-perspective projection (parallelProjection,
setParallelProjection). That concept is a renderer-matrix setting and lives
inside the resolved ICamera payload (rendererCamera.parallelProjection).
Both terms can coexist in the same code path:
// Cross-viewport projection adapter — this file's subject.
const snapshot = viewportProjection.get(viewport);
// VTK projection matrix — unrelated, set on the renderer camera.
snapshot?.rendererCamera?.parallelProjection; // boolean
If you are reading code that mentions "projection," check the noun: a
ProjectionSnapshot, ViewportProjectionAdapter, or viewportProjection
refers to the cross-viewport seam below. A parallelProjection flag or
setParallelProjection call refers to VTK's render-matrix mode.
Public Contract And Stability
The stable entry point is the projection service and the generic projection types:
viewportProjectionViewportProjectionServiceViewportProjectionTypes.tsProjectionSnapshotProjectionPresentationProjectionScaleProjectionPositionViewportProjectionAdapter
The family namespaces are intentionally exported as advanced helpers:
planarProjectionvolume3DProjectionvideoProjectionecgProjectionwsiProjection
Use those namespaces when building a custom synchronizer, tool, test, or new
viewport family that needs lower-level snapshot or renderer-camera behavior.
They are a tier below the core viewport methods and may change while the
Generic Viewport API is still settling. Application code should prefer
viewportProjection.getPresentation() and
viewportProjection.withPresentation() for presentation reads and writes.
Direct Next viewport instances intentionally do not expose
getViewPresentation() or setViewPresentation().
Core Types
The projection interface lives in ViewportProjectionTypes.ts.
interface ViewportProjectionAdapter<TViewState, TPresentation> {
id: string;
viewportTypes: string[];
getSnapshot(request: ProjectionRequest): ProjectionSnapshot | undefined;
getPresentation(
snapshot: ProjectionSnapshot,
selector?: ViewPresentationSelector
): TPresentation;
withPresentation(
snapshot: ProjectionSnapshot,
presentation: Partial<TPresentation>,
options?: ProjectionWriteOptions
): TViewState;
applyToRenderer?(snapshot: ProjectionSnapshot, target: unknown): void;
}
A ProjectionSnapshot is capability-based:
interface ProjectionSnapshot {
kind: string;
frameOfReferenceUID?: string;
spaces: {
canvas?: boolean;
world?: boolean;
image?: boolean;
renderer?: boolean;
};
transforms?: {
canvasToWorld?(point: Point2): Point3;
worldToCanvas?(point: Point3): Point2;
};
presentation: ProjectionPresentation;
rendererCamera?: ICamera;
}
If a viewport cannot provide a transform, it should omit that capability. Do not add placeholder transforms just to satisfy a universal shape.
Semantic Scale And Position
Projection scale and position are tagged so callers can read intent before using values:
type ProjectionScale =
| { kind: 'fit'; value: number }
| { kind: 'fitWidth'; value: number }
| { kind: 'fitHeight'; value: number }
| { kind: 'displayArea'; value: number; area: DisplayArea }
| { kind: 'nativePixel'; pixelsPerCanvasPixel: number }
| { kind: 'physical'; mmPerCanvasPixel: number }
| {
kind: 'signal';
samplesPerCanvasPixel: number;
valueUnitsPerCanvasPixel: number;
};
type ProjectionPosition =
| { kind: 'anchor'; worldPoint?: Point3; canvasPoint: Point2 }
| { kind: 'imagePoint'; imagePoint: Point2; canvasPoint: Point2 }
| { kind: 'mediaPoint'; mediaPoint: Point2; canvasPoint: Point2 }
| {
kind: 'signalPoint';
sampleIndex: number;
value: number;
channelIndex: number;
canvasPoint: Point2;
}
| { kind: 'focalPoint'; worldPoint: Point3 };
Do not treat presentation.zoom, presentation.scale, or presentation.pan
as universal values. Use the tag first, then branch on the semantics your
tool or synchronizer supports.
Projection Service
The package-level projection service is exported as viewportProjection.
import { viewportProjection } from '@cornerstonejs/core';
const projection = viewportProjection.get(viewport, {
kind: 'planar',
dataId,
});
The service is package/global, not per rendering engine. That keeps custom synchronizers and advanced tools independent from rendering-engine ownership.
Built-in viewport types and explicit kind requests have typed helper aliases
for downstream code:
import type {
ProjectionPresentationForKind,
ProjectionSnapshotForKind,
ProjectionViewStateForKind,
} from '@cornerstonejs/core';
type PlanarSnapshot = ProjectionSnapshotForKind<'planar'>;
type PlanarPresentation = ProjectionPresentationForKind<'planar'>;
type PlanarViewState = ProjectionViewStateForKind<'planar'>;
When the viewport instance has a literal Next viewport type, the service can
infer those same types from the viewport argument. Explicit kind requests are
available for custom synchronizers that only know a viewport as unknown.
Built-in adapters are registered for:
planarProjectionvolume3DProjectionvideoProjectionecgProjectionwsiProjection
The advanced namespaces expose lower-level helpers for code that intentionally works below the core viewport API.
createZoomPanSynchronizer in @cornerstonejs/tools already uses the service
when both source and target viewports expose projection adapters, then falls
back to the older getZoom/setZoom and getPan/setPan capability checks
for legacy viewport families.
When To Care
Most application code should use setViewState for native viewport mutation,
resetViewState for the viewport family's default navigation reset,
getViewReference / setViewReference for spatial navigation, and
canvasToWorld / worldToCanvas for coordinate conversion. Use projection when
code needs a portable presentation layer across viewport families.
Use Viewport Projection when you are writing:
- a custom synchronizer that needs to work across viewport families
- a tool that must inspect capabilities before transforming points
- a new Generic Viewport family
- a bridge between semantic state and renderer-specific camera output
Reading A Projection
Check capabilities before using transforms:
const projection = viewportProjection.get(viewport);
if (projection?.spaces.canvas && projection.spaces.world) {
const worldPoint = projection.transforms?.canvasToWorld?.([100, 120]);
}
Check scale and position semantics before applying them:
const scale = projection?.presentation.scale;
if (scale?.kind === 'displayArea') {
syncDisplayArea(scale.area);
}
if (scale?.kind === 'physical') {
syncPhysicalSpacing(scale.mmPerCanvasPixel);
}
Writing Presentation
Use withPresentation when you need the adapter to translate a presentation
patch back into semantic state:
const nextState = viewportProjection.withPresentation(viewport, {
zoom: 2,
pan: [10, -5],
});
if (nextState) {
viewport.setViewState(nextState);
}
Next viewports intentionally do not expose setViewPresentation. The
projection service is the portable write layer, and setViewState remains the
only Next viewport mutation primitive. They also do not expose
getViewPresentation; use viewportProjection.getPresentation(viewport, { selector }) instead. Legacy compatibility adapters may still expose
getViewPresentation and setViewPresentation for older code, but those
adapters are a temporary migration layer and their legacy camera/presentation
methods should be expected to disappear in a later breaking release.
Before, legacy or compatibility code might do this:
const presentation = viewport.getViewPresentation({
pan: true,
zoom: true,
});
viewport.setViewPresentation({
zoom: presentation.zoom * 2,
});
Direct Next code should do this:
const presentation = viewportProjection.getPresentation(viewport, {
selector: {
pan: true,
zoom: true,
},
});
const nextViewState = viewportProjection.withPresentation(viewport, {
zoom: (presentation?.zoom ?? 1) * 2,
});
if (nextViewState) {
viewport.setViewState(nextViewState);
}
Do not make a custom projection adapter mutate its viewport. It should return
native view state and let the caller decide whether to call setViewState.
Adding A New Adapter
For a new Generic Viewport family:
- Define the family-specific snapshot and presentation types.
- Implement
getSnapshotfrom current semantic state and resolved geometry. - Implement
getPresentationfor the existing public view-presentation shape. - Implement
withPresentationas a pure translation back to semantic state. - Implement
applyToRendereronly if the viewport can produce renderer output. - Register the adapter in the Generic Viewport projection setup.
The adapter should not mutate the viewport in withPresentation. Return the
next semantic state and let the viewport decide how to apply it.
Current Adapters
Planar projection uses:
PlanarViewStateas semantic statePlanarSliceBasisand resolved view geometry for data/world/canvas mappingPlanarResolvedICameraonly as renderer output- compatibility helpers for legacy
getZoom,getPan, andgetScale
Volume3D projection uses:
- the current runtime VTK camera as its state source
- focal point as semantic position when available
- physical canvas spacing derived from
parallelScale ICameraas renderer output
Video projection uses:
VideoViewStateas semantic state- intrinsic media-pixel coordinates for world/canvas conversion
mediaPointposition tagsnativePixelscale tags- optional renderer-camera output for legacy interop
ECG projection uses:
ECGViewStateas semantic state- signal tuples shaped as
[sampleIndex, amplitudeValue, channelIndex] signalPointposition tagssignalscale tags with samples and value units per canvas pixel- optional renderer-camera output for legacy interop
WSI projection uses:
WSIViewStatesynchronized from OpenLayers- slide/world transforms from the WSI resolved view
anchorposition tags- physical scale when renderer-camera output can provide it
This gives cross-viewport callers one projection interface while preserving the real differences between viewport families.