Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
6abe023
feat(core): add appConfig.useNextViewports flag (Generic Viewport M0 …
sedghi Jun 17, 2026
6c1aad1
feat(cornerstone): map viewport types to native *_NEXT under useNextV…
sedghi Jun 17, 2026
ef25b55
feat(cornerstone): native-next stack mount behind useNextViewports (M…
sedghi Jun 17, 2026
a3fa6ad
feat(cornerstone): render native Generic (next) stack viewport behind…
sedghi Jun 17, 2026
13af1cd
feat(cornerstone): render native Generic (next) volume/MPR behind use…
sedghi Jun 17, 2026
f78d01e
feat(cornerstone): allow setViewportOrientation on native volume view…
sedghi Jun 17, 2026
e164f8c
feat: Add temporary support for native GenericViewport ("next") migra…
sedghi Jun 17, 2026
6d9e46f
Add plan viewer HTML page for migration master plan display
sedghi Jun 18, 2026
ddd4ccc
feat(cornerstone): fork viewport presentation read/write into the bac…
sedghi Jun 18, 2026
c51fe4c
feat(cornerstone): persist native viewport pan/zoom/rotation/flip via…
sedghi Jun 18, 2026
93e15bd
feat(cornerstone): restore native volume/MPR pan/zoom on mount
sedghi Jun 18, 2026
99b23cb
fix(next): bridge invert/flip/window-level commands for native viewports
sedghi Jun 18, 2026
4a1b964
fix(next): apply setViewportColormap on native viewports
sedghi Jun 18, 2026
461c015
fix(next): make ColorbarService native-safe
sedghi Jun 18, 2026
d282026
fix(next): guard per-volume histogram WL panel for native viewports
sedghi Jun 18, 2026
55091cf
fix(next): make resetViewport/scaleViewport/rotate commands native-safe
sedghi Jun 18, 2026
26273b3
fix(next): guard jumpToMeasurement camera-centering for native viewports
sedghi Jun 18, 2026
5d39eca
fix(next): make getViewportAlignmentData + updateViewport native-safe
sedghi Jun 18, 2026
984e6ac
refactor(next): move viewport interaction ops into a Legacy/Next oper…
sedghi Jun 19, 2026
3963b57
feat(next): render native 3D volume rendering + enable its VR operations
sedghi Jun 19, 2026
05e0df0
docs(next): refresh migration plan to HEAD (2026-06-19 audit)
sedghi Jun 19, 2026
7b61e08
fix(next): target fusion colormap at the overlay binding
sedghi Jun 19, 2026
a19bd78
fix(next): make residual native-unsafe viewport sites safe
sedghi Jun 19, 2026
ca746e2
feat(next): mount native video/WSI/ECG viewports
sedghi Jun 19, 2026
b5784ca
chore(next): guard the useNextViewports flag-read allowlist (M7 prep)
sedghi Jun 19, 2026
d5d03d8
docs(next): mark CS-12 native calibration done; refresh CS-20/M6 status
sedghi Jun 20, 2026
138dc17
docs(next): re-verify migration status at HEAD; correct stale prose
sedghi Jun 20, 2026
aad8cb9
Refactor SegmentationService to support dual backends for segmentatio…
sedghi Jun 20, 2026
cc9883d
temp
sedghi Jun 22, 2026
ec3cba4
Refactor CornerstoneViewportService to optimize setDisplaySets handli…
sedghi Jun 22, 2026
55352f2
d
sedghi Jun 23, 2026
053f63c
Refactor viewport handling and opacity management for native volume r…
sedghi Jun 23, 2026
9cbe6b1
fix(WindowLevel): re-sync fusion tab to foreground default after asyn…
sedghi Jun 23, 2026
dd9e8c3
Merge remote-tracking branch 'origin/master' into ohifohifnextapi
sedghi Jun 23, 2026
b8efc7c
chore(next): remove WIP migration plan artifacts from the branch
sedghi Jun 23, 2026
880b346
fix(next): address review findings (guards + correctness)
sedghi Jun 23, 2026
fd2abfa
fix(overlay): show instance number on next viewports
sedghi Jun 23, 2026
0f9df44
fix(overlay): refresh window level on series change for next viewports
sedghi Jun 23, 2026
4374847
fix(scrollbar): seed slice state on orientation change for next viewp…
sedghi Jun 23, 2026
946671b
refactor(next): call resetDisplaySetPresentation on reset
sedghi Jun 23, 2026
1469efe
fix(segmentation): make border/outline thickness slider integer-only
sedghi Jun 23, 2026
7fadeea
fix(crosshairs): guard resetCrosshairs against unregistered Crosshair…
sedghi Jun 23, 2026
ec034e4
fix(next-fusion): promote source to volume slice when a data overlay …
sedghi Jun 24, 2026
03c2bf1
fix(next-rtss): keep referenced CT in stack mode on RTSTRUCT hydrate
sedghi Jun 24, 2026
b82ffc5
fix(next-fusion): match legacy initial data-overlay opacity (~40%) on…
sedghi Jun 25, 2026
9571c5f
fix(next-fusion): preserve fusion on orientation change in next viewp…
sedghi Jun 25, 2026
b8c6f80
fix(next-seg): preserve base image window level through SEG hydration
sedghi Jun 25, 2026
a8be6fa
fix(next-mpr): re-seed slice scrollbar after post-mount camera carry
sedghi Jun 25, 2026
fa6ddde
Merge remote-tracking branch 'origin/master' into ohifohifnextapi
sedghi Jun 25, 2026
2389f55
Merge remote-tracking branch 'origin/master' into ohifohifnextapi
sedghi Jun 30, 2026
1ac4a09
chore(deps): bump @cornerstonejs/* to 5.1.2
sedghi Jun 30, 2026
e5c5f73
chore: empty commit
sedghi Jun 30, 2026
ee9bff2
fix(next): use published genericViewportDisplaySetMetadataProvider ex…
sedghi Jun 30, 2026
9153297
test(e2e): extend Scoord3dProbe jump screenshot retry window
sedghi Jun 30, 2026
3392e02
fix(next): address review findings (scaleBy 3D crash, fusion W/L targ…
sedghi Jun 30, 2026
b16129e
fix(next): address CodeRabbit review (dispatch discriminator, seg ret…
sedghi Jun 30, 2026
d0c8d50
test(next): update getCornerstoneViewportType invalid-type assertion
sedghi Jun 30, 2026
084b6b9
chore(next): replace dev toggle + flag-read guard with URL opt-in
sedghi Jun 30, 2026
dab2f1b
fix(tmtv): keep legacy fusion PT opacity ramp; flatten only on next path
sedghi Jul 1, 2026
113a20e
redo
sedghi Jul 2, 2026
812e823
Merge remote-tracking branch 'origin/master' into ohifohifnextapi
sedghi Jul 2, 2026
641dac1
fix(next): avoid top-level dicomWebUtils destructure crash on boot
sedghi Jul 2, 2026
df7aa4b
fix(next): guard remount no-op path and defer legacy camera snapshot
sedghi Jul 2, 2026
9e5ab84
feat(next): add ?cpu=true URL opt-in to force the CPU render path
sedghi Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 81 additions & 37 deletions extensions/cornerstone-dicom-seg/src/commandsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ const {
},
} = adaptersRT;


const commandsModule = ({
servicesManager,
extensionManager,
Expand Down Expand Up @@ -91,46 +90,69 @@ const commandsModule = ({
const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId);
const predecessorImageId = options.predecessorImageId ?? segmentation.predecessorImageId;

const { imageIds } = segmentation.representationData.Labelmap;

const segImages = imageIds.map(imageId => cache.getImage(imageId));
const referencedImages = segImages.map(image => cache.getImage(image.referencedImageId));

const labelmaps2D = [];

let z = 0;
const labelmapData = segmentation.representationData.Labelmap;

// Build a labelmap3D (one labelmaps2D entry per source slice) from a list of
// derived labelmap image ids. When `referencedImageIds` is supplied (the
// multi-layer/overlap path) each frame is indexed by its source slice so the
// layers align to the same frames; otherwise frames are sequential (the legacy
// single-layer behavior, kept byte-identical).
const buildLabelmap3D = (segImageIds: string[], metadata, referencedImageIds?: string[]) => {
const segImages = segImageIds.map(imageId => cache.getImage(imageId));
const labelmaps2D = [];

// Map each source imageId to its frame index once (O(n)) so the per-slice lookup
// below is O(1) — avoids the O(slices^2) indexOf scan on the multi-layer path.
const referencedFrameIndexById = referencedImageIds
? new Map(referencedImageIds.map((imageId, index) => [imageId, index]))
: undefined;

let z = 0;

for (const segImage of segImages) {
const segmentsOnLabelmap = new Set();
const pixelData = segImage.getPixelData();
const { rows, columns } = segImage;

// Use a single pass through the pixel data
for (let i = 0; i < pixelData.length; i++) {
const segment = pixelData[i];
if (segment !== 0) {
segmentsOnLabelmap.add(segment);
}
}

for (const segImage of segImages) {
const segmentsOnLabelmap = new Set();
const pixelData = segImage.getPixelData();
const { rows, columns } = segImage;
const frameIndex = referencedFrameIndexById
? referencedFrameIndexById.get(segImage.referencedImageId) ?? -1
: z++;

// Use a single pass through the pixel data
for (let i = 0; i < pixelData.length; i++) {
const segment = pixelData[i];
if (segment !== 0) {
segmentsOnLabelmap.add(segment);
if (frameIndex < 0) {
continue;
}
}

labelmaps2D[z++] = {
segmentsOnLabelmap: Array.from(segmentsOnLabelmap),
pixelData,
rows,
columns,
};
}
labelmaps2D[frameIndex] = {
segmentsOnLabelmap: Array.from(segmentsOnLabelmap),
pixelData,
rows,
columns,
};
}

const allSegmentsOnLabelmap = labelmaps2D.map(labelmap => labelmap.segmentsOnLabelmap);
const allSegmentsOnLabelmap = labelmaps2D
.filter(Boolean)
.map(labelmap => labelmap.segmentsOnLabelmap);

const labelmap3D = {
segmentsOnLabelmap: Array.from(new Set(allSegmentsOnLabelmap.flat())),
metadata: [],
labelmaps2D,
return {
segmentsOnLabelmap: Array.from(new Set(allSegmentsOnLabelmap.flat())),
metadata,
labelmaps2D,
};
};

// Segment metadata (shared across all layers).
const segmentationInOHIF = segmentationService.getSegmentation(segmentationId);
const representations = segmentationService.getRepresentationsForSegmentation(segmentationId);
const metadata = [];

Object.entries(segmentationInOHIF.segments).forEach(([segmentIndex, segment]) => {
// segmentation service already has a color for each segment
Expand All @@ -151,7 +173,7 @@ const commandsModule = ({
color.slice(0, 3).map(value => value / 255)
).map(value => Math.round(value));

const segmentMetadata = {
metadata[segmentIndex] = {
SegmentNumber: segmentIndex.toString(),
SegmentLabel: label,
SegmentAlgorithmType: segment?.algorithmType || 'MANUAL',
Expand All @@ -168,10 +190,34 @@ const commandsModule = ({
CodeMeaning: 'Tissue',
},
};
labelmap3D.metadata[segmentIndex] = segmentMetadata;
});

const generatedSegmentation = generateSegmentation(referencedImages, labelmap3D, metaData, {
// Multi-layer (overlapping) SEGs register one labelmap layer per conflict-free
// group. Export each layer as its own labelmap3D against the UNIQUE referenced
// source series, so cornerstone writes overlapping segments as separate frames
// that reference the same source slice (the DICOM SEG overlap encoding). The
// cs3D adapter's fillSegmentation accepts an array of labelmap3D for exactly
// this. Single-layer SEGs keep the original single-labelmap3D path unchanged.
const layers = labelmapData.labelmaps ? Object.values(labelmapData.labelmaps) : undefined;

let referencedImages;
let labelmaps3D;

if (layers && layers.length > 1) {
const referencedImageIds =
layers[0].referencedImageIds ?? labelmapData.referencedImageIds ?? [];
referencedImages = referencedImageIds.map(imageId => cache.getImage(imageId));
labelmaps3D = layers.map(layer =>
buildLabelmap3D(layer.imageIds ?? [], metadata, referencedImageIds)
);
} else {
const { imageIds } = labelmapData;
const segImages = imageIds.map(imageId => cache.getImage(imageId));
referencedImages = segImages.map(image => cache.getImage(image.referencedImageId));
labelmaps3D = buildLabelmap3D(imageIds, metadata);
}

const generatedSegmentation = generateSegmentation(referencedImages, labelmaps3D, metaData, {
predecessorImageId,
...options,
});
Expand Down Expand Up @@ -240,9 +286,7 @@ const commandsModule = ({
}

const defaultFileName =
modality === 'RTSTRUCT'
? `rtss-${segmentationId}.dcm`
: `${label || 'segmentation'}.dcm`;
modality === 'RTSTRUCT' ? `rtss-${segmentationId}.dcm` : `${label || 'segmentation'}.dcm`;

const storeFn = commandsManager.runCommand('createStoreFunction', {
dataSource: dataSourceName,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { vec3 } from 'gl-matrix';
import PropTypes from 'prop-types';
import { metaData, Enums, utilities, eventTarget } from '@cornerstonejs/core';
import { metaData, Enums, eventTarget } from '@cornerstonejs/core';
import { Enums as csToolsEnums, UltrasoundPleuraBLineTool } from '@cornerstonejs/tools';
import type { ImageSliceData } from '@cornerstonejs/core/types';
import { ViewportOverlay, formatDICOMDate } from '@ohif/ui-next';
import type { InstanceMetadata } from '@ohif/core/src/types';
import { formatDICOMTime, formatNumberPrecision } from './utils';
import { utils } from '@ohif/core';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
import { getViewportAdapter } from '../../services/ViewportService/adapter';
import { getViewportDataShapeType } from '../../utils/viewportDataShape';

import './CustomizableViewportOverlay.css';
import { useViewportRendering } from '../../hooks';
Expand Down Expand Up @@ -264,7 +266,7 @@ function getDisplaySets(viewportData, displaySetService) {
const getInstanceNumber = (viewportData, viewportId, imageIndex, cornerstoneViewportService) => {
let instanceNumber;

switch (viewportData.viewportType) {
switch (getViewportDataShapeType(viewportData)) {
case Enums.ViewportType.STACK:
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
break;
Expand Down Expand Up @@ -331,8 +333,11 @@ function _getInstanceNumberFromVolume(
return;
}

const camera = cornerstoneViewport.getCamera();
const { viewPlaneNormal } = camera;
const viewPlaneNormal = getViewportAdapter(cornerstoneViewport).getViewPlaneNormal();

if (!viewPlaneNormal) {
return;
}
// checking if camera is looking at the acquisition plane (defined by the direction on the volume)

const scanAxisNormal = direction.slice(6, 9);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { Enums, utilities as csUtils } from '@cornerstonejs/core';
import { utilities as csUtils } from '@cornerstonejs/core';
import { ImageScrollbar } from '@ohif/ui-next';
import { isVolume3DViewportType } from '../../utils/getLegacyViewportType';
import { getSliceEventName, getViewportSliceCount } from '../../utils/viewportDataShape';

function CornerstoneImageScrollbar({
viewportData,
Expand Down Expand Up @@ -47,10 +48,10 @@ function CornerstoneImageScrollbar({

try {
const imageIndex = viewport.getCurrentImageIdIndex();
const numberOfSlices = viewport.getNumberOfSlices();
const numberOfSlices = getViewportSliceCount(viewportData, viewport);

setImageSliceData({
imageIndex: imageIndex,
imageIndex,
numberOfSlices,
});
} catch (error) {
Expand All @@ -62,11 +63,7 @@ function CornerstoneImageScrollbar({
if (!viewportData) {
return;
}
const { viewportType } = viewportData;
const eventId =
(viewportType === Enums.ViewportType.STACK && Enums.Events.STACK_NEW_IMAGE) ||
(viewportType === Enums.ViewportType.ORTHOGRAPHIC && Enums.Events.VOLUME_NEW_IMAGE) ||
Enums.Events.IMAGE_RENDERED;
const eventId = getSliceEventName(viewportData);

const updateIndex = event => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { vec3 } from 'gl-matrix';

import './ViewportOrientationMarkers.css';
import { useViewportRendering } from '../../hooks';
import { getViewportDataShapeType } from '../../utils/viewportDataShape';
const { getOrientationStringLPS, invertOrientationStringLPS } = utilities.orientation;

function ViewportOrientationMarkers({
Expand Down Expand Up @@ -46,7 +47,9 @@ function ViewportOrientationMarkers({
return '';
}

if (viewportData.viewportType === 'stack') {
// Use the persisted data shape, not viewportType: a native stack reports
// PLANAR_NEXT, which would skip this synthetic-IOP default-cosine guard.
if (getViewportDataShapeType(viewportData) === Enums.ViewportType.STACK) {
const imageIndex = imageSliceData.imageIndex;
const imageId = viewportData.data[0].imageIds?.[imageIndex];

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Enums } from '@cornerstonejs/core';
import { ViewportData } from './types';
import { isVolume3DViewportType } from '../../../utils/getLegacyViewportType';
import { getViewportAdapter } from '../../../services/ViewportService/adapter';

export function getImageIndexFromEvent(event): number | undefined {
const { imageIndex, newImageIdIndex = imageIndex, imageIdIndex } = event.detail;
Expand All @@ -24,12 +24,17 @@ export function isProgressFullMode(viewportData: ViewportData, viewport): boolea
return false;
}

if (viewportData.viewportType === Enums.ViewportType.STACK) {
// A stack renders the full progress UI; an acquisition-plane volume is the
// volume-mode equivalent. The adapter classifies both lanes (legacy by
// viewport type / isInAcquisitionPlane; native by content mode + view-state
// orientation, since PLANAR_NEXT collapses the runtime type).
const adapter = getViewportAdapter(viewport);
const shape = adapter.getShape();
if (shape === 'stack') {
return true;
}

if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
return !!viewport.isInAcquisitionPlane?.();
if (shape === 'volume') {
return adapter.isInAcquisitionPlane();
}

return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { useEffect, useRef, useState } from 'react';
import {
cache as cornerstoneCache,
Enums,
eventTarget,
utilities,
} from '@cornerstonejs/core';
import { cache as cornerstoneCache, Enums, eventTarget, utilities } from '@cornerstonejs/core';
import { useByteArray } from '@ohif/ui-next';
import { isVolume3DViewportType } from '../../../utils/getLegacyViewportType';
import { getSliceEventName, getViewportSliceCount } from '../../../utils/viewportDataShape';
import { getImageIdFromCacheEvent, getImageIndexFromEvent, isProgressFullMode } from './helpers';
import { ImageSliceData, ViewportData } from './types';

Expand Down Expand Up @@ -97,26 +93,48 @@ export function useViewportSliceSync({
return;
}

const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport && !isVolume3DViewportType(viewport)) {
// Last values we pushed, so re-seeding on camera changes does not churn React
// state on pure pan/zoom (which keep the slice geometry unchanged).
const lastSlice = { imageIndex: -1, numberOfSlices: -1 };

const pushSliceData = (imageIndex: number, numberOfSlices: number) => {
if (imageIndex === lastSlice.imageIndex && numberOfSlices === lastSlice.numberOfSlices) {
return;
}
lastSlice.imageIndex = imageIndex;
lastSlice.numberOfSlices = numberOfSlices;
setImageSliceData({ imageIndex, numberOfSlices });
};

// Seeds the shared slice state from the live viewport. Re-run on the initial
// effect and on camera/orientation changes (below).
const syncFromViewport = () => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || isVolume3DViewportType(viewport)) {
return;
}
try {
const currentImageIndex = viewport.getCurrentImageIdIndex();
const currentNumberOfSlices = viewport.getNumberOfSlices();
const currentNumberOfSlices = getViewportSliceCount(viewportData, viewport);

setImageSliceData({
imageIndex: currentImageIndex,
numberOfSlices: currentNumberOfSlices,
});
pushSliceData(currentImageIndex, currentNumberOfSlices);
} catch (error) {
console.warn(error);
}
}
};

const { viewportType } = viewportData;
const eventId =
(viewportType === Enums.ViewportType.STACK && Enums.Events.STACK_NEW_IMAGE) ||
(viewportType === Enums.ViewportType.ORTHOGRAPHIC && Enums.Events.VOLUME_NEW_IMAGE) ||
Enums.Events.IMAGE_RENDERED;
syncFromViewport();

// A post-mount camera carry (e.g. the layout-selector MPR protocol restoring
// the prior stack slice onto the freshly-mounted volume viewport) moves the
// camera and fires its slice events synchronously during the mount — before
// these listeners attach and around the initial seed above — so the scrollbar
// can latch the mount-time index instead of the carried slice. Re-seed once on
// the next frame, after the mount+carry settles; pushSliceData makes it a
// no-op when nothing changed (no churn/flicker).
const reseedRaf = requestAnimationFrame(syncFromViewport);

const eventId = getSliceEventName(viewportData);

const updateIndex = event => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
Expand All @@ -130,16 +148,22 @@ export function useViewportSliceSync({
}
const nextNumberOfSlices = viewport.getNumberOfSlices();

setImageSliceData({
imageIndex: nextImageIndex,
numberOfSlices: nextNumberOfSlices,
});
pushSliceData(nextImageIndex, nextNumberOfSlices);
};

element.addEventListener(eventId, updateIndex);
// Native ("next") viewports keep the same viewportData across a stack->volume
// transition or an orientation change, so this effect does not re-run and the
// slice-navigation event above may not fire until the first scroll, leaving the
// scrollbar unseeded (or stale, with a now-wrong slice count). CAMERA_MODIFIED
// fires on those orientation/geometry changes, so re-seed from the viewport
// then; the pushSliceData guard makes pan/zoom (same geometry) a no-op.
element.addEventListener(Enums.Events.CAMERA_MODIFIED, syncFromViewport);

return () => {
cancelAnimationFrame(reseedRaf);
element.removeEventListener(eventId, updateIndex);
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, syncFromViewport);
};
}, [viewportData, element, viewportId, cornerstoneViewportService, setImageSliceData]);
}
Expand Down
Loading
Loading