Compare commits

...

230 Commits

Author SHA1 Message Date
bbe37ff388 feat: annotation persistence (local storage)
Some checks failed
Build and Deploy Docs / build-and-deploy-docs (push) Has been cancelled
CodeQL Advanced / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL Advanced / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
2026-07-16 10:38:36 +01:00
ohif-bot
f7612cdd0a chore(version): Update package versions to 3.13.0-beta.125 [skip ci] 2026-07-14 23:14:16 +00:00
Alireza
125a174298
fix(cli): link and unlink extensions/modes with pnpm instead of yarn (#6144) 2026-07-14 19:10:49 -04:00
ohif-bot
b880d2acf9 chore(version): Update package versions to 3.13.0-beta.124 [skip ci] 2026-07-13 16:59:27 +00:00
Dan Rukas
03e17d764a
fix(ui): clean up remaining legacy ui usage (#6140)
* Update button on DataSourceSelector

* Removed dead code ToolbarButtonNestedMenu, button spacing
2026-07-13 12:56:17 -04:00
Dan Rukas
3d70db2d3b
ui(ContextMenuViewport): move to ui-next with theme support (#6008) 2026-07-13 12:56:03 -04:00
ohif-bot
110b293aa0 chore(version): Update package versions to 3.13.0-beta.123 [skip ci] 2026-07-11 16:19:35 +00:00
Alireza
70421225d7
fix(pt): resolve Philips PET private SUV bulkdata before scaling (#6096)
* fix(pt): resolve Philips PET private SUV bulkdata before scaling

When a DICOMweb server delivers the Philips PET private tags SUVScaleFactor
(7053,1000) / ActivityConcentrationScaleFactor (7053,1009) as bulkdata, dcmjs
naturalization leaves them as { BulkDataURI } objects. These were fed verbatim
to calculate-suv, which treats the object as a valid value and silently
corrupts the SUV scaling factors.

Resolve these scalar private tags to numbers during ingestion - in both the
lazy (async) and non-lazy (sync) DICOMweb metadata paths, before INSTANCES_ADDED
fires - by decoding the bulkdata (VR-aware: DS/IS text or little-endian FL/FD).
Harden getPTImageIdInstanceMetadata to coerce values to finite numbers (reusing
@ohif/core utils.toNumber) and reject unresolved bulkdata objects so they can
never reach calculate-suv. Share the bulkdata-attach helper between both
metadata paths. Adds unit tests for the bulkdata decoder/resolver and for
getPTImageIdInstanceMetadata.

* refactor(bulkdata): move PET bulkdata resolution to a generic core tag registry

Generalize resolvePETPrivateScalarBulkData into a datasource-agnostic
utils.resolveBulkDataTags in @ohif/core, backed by a static tag registry
seeded with the Philips PET SUV/activity-concentration scalar tags and
extensible via registerResolvedBulkDataTags.

* fix(bulkdata): strip NUL padding and refresh qido auth before resolution

Address review feedback:
- decodeText now strips NUL (0x00) padding, which String.trim() leaves
  intact, so NUL-padded DS/IS values no longer decode as NaN. Adds a
  regression test.
- refresh qidoDicomWebClient.headers before resolveBulkDataTags in both
  series-metadata paths; retrieveBulkData is bound to qidoDicomWebClient,
  matching every other qido op in this file.

* fix(dicomweb): await deferred metadata storage

* fix(dicomweb): support single-part bulkdata responses
2026-07-11 12:16:32 -04:00
Alireza
3c0e245398
feat(next): native Generic Viewport migration (useNextViewports) (#6101)
* feat(core): add appConfig.useNextViewports flag (Generic Viewport M0 step 1)

Opt-in flag to drive viewports through the DIRECT native cornerstone3D
GenericViewport ("next") API surface (PLANAR_NEXT / VOLUME_3D_NEXT, setDisplaySets,
setDisplaySetPresentation, setViewState, view references) instead of the legacy
stack/volume methods.

Distinct from (and overrides) useGenericViewport, which only routes legacy
viewport types through cornerstone compatibility adapters. This flag does NOT set
cornerstone rendering.useGenericViewport; it is read by getCornerstoneViewportType
and the CornerstoneViewportService backend split (subsequent M0 steps). Defaults
false; the legacy path stays byte-identical. Opt-in only.

* feat(cornerstone): map viewport types to native *_NEXT under useNextViewports (M0 step 2)

getCornerstoneViewportType gains an optional useNextViewports param. When set,
stack/volume/orthographic collapse to PLANAR_NEXT (render path inferred from data
shape), and volume3d/video/wholeslide/ecg map to their VOLUME_3D_NEXT / VIDEO_NEXT /
WHOLE_SLIDE_NEXT / ECG_NEXT types. Defaults false → legacy mapping byte-identical;
no caller passes true yet (wired via appConfig in the service split, step 3).

Tests: +6 cases (21 total) covering the *_NEXT mapping, displaySet override, the
invalid-type throw, and that the legacy mapping is unchanged when the flag is off.

* feat(cornerstone): native-next stack mount behind useNextViewports (M0 step 3 foundation)

Wires the useNextViewports flag through and mounts stack viewports natively:
- nextViewports.ts: module accessor; init.tsx captures appConfig.useNextViewports.
- getCornerstoneViewportType: defaults the flag from the accessor, and now passes
  native (*_NEXT) types through idempotently (a viewport's stored cs type is
  re-fed into the mapper; legacy types were already idempotent, native were not).
- CornerstoneViewportService._setDisplaySets: route native generic viewports by
  data shape (StackData vs VolumeData), since PLANAR_NEXT is one type for both.
- _setStackViewport: native branch mounts via genericViewportDataSetMetadataProvider.add
  + setDisplaySets, applies VOI/colormap via setDisplaySetPresentation and
  displayArea/rotation/flip via setViewState (no legacy setStack/setProperties/setCamera).

Validated in a running OHIF (linked cornerstone 5.0.8): with the flag on, the
viewer creates a native PlanarViewport (window.cornerstone...getViewports()[0] ->
'PlanarViewport :: type=planarNext'); the prior 'Invalid viewport type: planarNext'
is resolved. Flag OFF is byte-identical (native branches gated by isGenericViewport).

KNOWN WIP (next): flag-ON full render is blocked by the presentation-read seam —
peripheral consumers (useViewportRendering, overlays, colorbar, resize) still call
legacy getProperties/getViewPresentation/getCamera on the viewport. Stack render
completes once those are routed through getDisplaySetPresentation/viewportProjection.

* feat(cornerstone): render native Generic (next) stack viewport behind useNextViewports

Makes the flag-on native PLANAR_NEXT stack render end-to-end in OHIF:

- CornerstoneCacheService: resolve the stack-vs-volume data-builder from the
  legacy mapping, since native types collapse that distinction into PLANAR_NEXT.
  Without this the stack fell through to the 'other' builder and imageIds were
  never populated (PlanarViewport threw 'No registered planar dataset metadata').
- ImageOverlayViewerTool: skip overlay rendering when the viewport has no
  resolvable view reference yet (native returns falsy until data is bound),
  instead of letting getTargetId() throw and kill the route during enable.
- Add getViewportPresentation helpers (getViewportProperties / getViewportCameraState)
  bridging legacy getProperties/getCamera and native getDisplaySetPresentation/
  getViewState; apply at the toolbar property evaluator, VOI-range init, and the
  position/LUT presentation snapshots so reads no longer throw on native viewports.

Validated in a running OHIF (flag on): PlanarViewport(planarNext), 295 slices,
image renders, zero console errors, scroll + setImageIdIndex navigate correctly.
Legacy (flag off) behavior is unchanged. Native volume/MPR is a later increment.

* feat(cornerstone): render native Generic (next) volume/MPR behind useNextViewports

Mounts volumes on a direct PLANAR_NEXT viewport for volume/MPR rendering:

- CornerstoneViewportService.setVolumesForViewport: native branch -> new
  _setNativeVolumeDisplaySets. Each base volume is registered with its
  already-cached volumeId and bound via setDisplaySets at the viewport's
  orientation (first = source, others = overlay); VOI/colormap/invert applied
  per-binding via setDisplaySetPresentation(dataId, props). Skips the legacy
  setVolumes/setProperties/setPresentations surface a PLANAR_NEXT viewport does
  not expose. Cornerstone reuses the OHIF-cached volume (getVolumeId returns the
  passed volumeId) and selects the image vs reformatted-volume render path from
  the requested orientation.
- useViewportRendering colormap resolver: read via getViewportProperties for
  native viewports (getDisplaySetPresentation) instead of getProperties/getActors,
  which threw 'Error getting viewport colormap' on native volume.

Validated in a running OHIF (flag on): axial/sagittal/coronal all render in
volume mode, scroll navigates the volume, round-trip volume<->stack switches
getCurrentMode cleanly, zero console errors. Legacy (flag off) unchanged.

* feat(cornerstone): allow setViewportOrientation on native volume viewports

The setViewportOrientation command guarded on isOrthographicViewportType, which
is false for a native PLANAR_NEXT viewport (it reports type=planarNext even when
rendering MPR). Add the content-mode capability guard (csUtils.viewportIsInVolumeMode)
so the MPR orientation toolbar works on native viewports; PlanarViewport.setOrientation
already exists. Legacy behavior is unchanged (viewportIsInVolumeMode is false for
legacy viewports, so the existing isOrthographicViewportType branch still gates them).

Also flips useNextViewports on in the dev default config for local testing of the
native path (NOT for merge; see TODO_BEFORE_MERGE).

* feat: Add temporary support for native GenericViewport ("next") migration

- Introduced a dev-only configuration flag to toggle the native viewport backend.
- Added a toolbar button to switch between legacy and native viewports for debugging.
- Implemented logic to handle image slice data and viewport type detection for native viewports.
- Enhanced viewport service to derive default VOI window/level from DICOM metadata.
- Added utility functions for managing localStorage overrides for viewport settings.
- Marked all temporary changes with comments for easy identification and removal before merging.

* Add plan viewer HTML page for migration master plan display

* feat(cornerstone): fork viewport presentation read/write into the backend (§4.3)

Extends the legacyBackend/nextBackend seam so presentation read/write is forked,
not inline-guarded in the service:

- IViewportBackend gains getPositionPresentation / setPositionPresentation /
  setLutPresentation. LegacyViewportBackend keeps the exact legacy logic
  (getViewPresentation / setProperties / setViewPresentation) byte-identical;
  NextViewportBackend uses the native surface (getViewReference + setViewReference;
  setDisplaySetPresentation for VOI/colormap/invert). WITH_ORIENTATION is inlined in
  the backends to avoid a backend->service value-import cycle.
- CornerstoneViewportService._getPositionPresentation / _setLutPresentation /
  _setPositionPresentation now delegate to this.backend. _getLutPresentation stays
  shared (already native-aware via the getViewportProperties bridge).

Effect: setPresentations is now native-safe — it previously threw on a PLANAR_NEXT
viewport (setProperties / setViewPresentation), so the native mount skipped it; the
native path now round-trips presentation cleanly. Native pan/zoom persistence
(viewPresentation is still undefined on native) is a later increment.

Validated both lanes: native PlanarViewport renders + storePresentation/getPresentations/
setPresentations round-trip with no crash; legacy StackViewport byte-identical
round-trip; zero console errors each.

* feat(cornerstone): persist native viewport pan/zoom/rotation/flip via the backend

A PLANAR_NEXT viewport has no getViewPresentation/setViewPresentation, so pan/zoom
previously did not survive navigation/resize/layout on the native path. NextViewportBackend
now snapshots the pan/zoom subset of the semantic view state and restores it:

- getPositionPresentation: snapshots the PlanarViewState pan/zoom fields (displayArea,
  anchorWorld, anchorCanvas, scale, scaleMode, rotation, flipHorizontal, flipVertical) via
  getViewState() (already deep-cloned + serializable), stored in viewPresentation. Slice and
  orientation are intentionally excluded (they ride on the view reference).
- setPositionPresentation: applies the view reference first (slice/orientation), then a partial
  setViewState patch with only the pan/zoom subset — the merge preserves slice/orientation, so
  the view reference is never clobbered. Stale displayArea is cleared when live anchor/scale
  pan/zoom is restored. anchorCanvas is canvas-fractional, so it survives resize without drift.
- _setStackViewport native branch now restores the persisted positionPresentation on mount
  (position-only; LUT already applied inline), so a returning stack recovers its camera.

Validated on native: zoom -> snapshot (scale captured) -> reset -> restore (scale back),
slice unchanged, zero errors. Legacy path unchanged. Volume/MPR mount pan/zoom restore is a
follow-up (its native mount helper does not yet thread presentations).

* feat(cornerstone): restore native volume/MPR pan/zoom on mount

Extends native pan/zoom persistence to the volume/MPR mount: the native branch of
setVolumesForViewport now restores the persisted positionPresentation (view reference
+ pan/zoom via the backend) after _setNativeVolumeDisplaySets, mirroring the stack
mount. Position-only (LUT applied per-binding above), native-safe.

Validated on a native sagittal MPR: zoom -> snapshot (scale 1.7) -> reset -> restore
(scale back to 1.7) with orientation (sagittal) and slice (256) preserved; zero errors.

* fix(next): bridge invert/flip/window-level commands for native viewports

Add setViewportProperties/setViewportCameraState write bridges alongside the
existing read bridges, and route invertViewport, flipViewportHorizontal,
flipViewportVertical and setViewportWindowLevel through them.

Direct PLANAR_NEXT viewports have no getCamera/setCamera/getProperties/
setProperties, so these four commands threw on native. The bridges dispatch on
isGenericViewport: native reads/writes via getViewState/setViewState (flip) and
getDisplaySetPresentation/setDisplaySetPresentation (invert/voiRange) on the
active binding; legacy falls through to the identical getCamera/setCamera/
getProperties/setProperties calls, so flag-off stays byte-identical.

Verified live on a native stack viewport: all four now apply (invert
undefined->true, flipH/flipV false->true, voiRange retargets) instead of
throwing.

* fix(next): apply setViewportColormap on native viewports

setViewportColormap was fully guarded by isStackViewportType/
isOrthographicViewportType, both of which report false for native PLANAR_NEXT
viewports, so the command was a silent no-op on native (returned ok but applied
nothing). Add a native branch that applies the colormap via the
setViewportProperties bridge (setDisplaySetPresentation) on the active binding,
honoring the immediate render flag, before the legacy guards.

Verified live: HSV colormap now applies and renders on a native stack viewport.

* fix(next): make ColorbarService native-safe

ColorbarService called getActors/getProperties/setProperties directly, all of
which throw on direct PLANAR_NEXT (next) viewports, so toggling a colorbar threw
on native. Replace the getActors content gate with a viewportHasContent helper
(getCurrentMode for native, getActors for legacy), and route the property
read/write through the getViewportProperties/setViewportProperties bridges.
Legacy keeps the identical getActors/getProperties/setProperties calls.

Verified live on native: addColorbar no longer throws, hasColorbar becomes true,
and the colormap applies via setDisplaySetPresentation.

* fix(next): guard per-volume histogram WL panel for native viewports

getWindowLevelsData drives the per-volume histogram WL panel via
getAllVolumeIds/getProperties, which direct PLANAR_NEXT (next) viewports do not
expose (they throw). Add an early guard that returns no rows when getAllVolumeIds
is absent, so the panel degrades to 'No window level data available' instead of
erroring on the interval/event refresh. Legacy stack viewports also lack
getAllVolumeIds and were never passed here, so this is a no-op for them; the
native stack/volume WL path is driven by setViewportWindowLevel.

* fix(next): make resetViewport/scaleViewport/rotate commands native-safe

These three command paths assumed legacy camera APIs that direct PLANAR_NEXT
(next) viewports do not expose:
- resetViewport called viewport.resetCamera() (absent on native -> threw). Native
  branch uses resetViewState() (resets pan/zoom/rotation/orientation/flip;
  navigation/slice preserved). resetProperties stays optional-chained.
- scaleViewport (scaleUpViewport/scaleDownViewport) was guarded by
  isStackViewportType, which is false for native, so the zoom buttons were a
  silent no-op. Native branch uses getZoom/setZoom; parallelScale and zoom are
  inversely related so it divides by scaleFactor to match legacy direction.
- _rotateViewport (rotateViewportCW/CCW/CWSet) used getViewPresentation/
  setViewPresentation (absent on native). Native branch reads rotation/flip via
  the getViewportCameraState bridge (getViewState) and writes the new rotation
  via setViewportCameraState (setViewState), preserving the flip-parity logic of
  the 'set' mode.

Verified live on native stack: reset no longer throws and resets zoom; zoom in
1->1.111; CW/CCW/Set rotation applies (90/180/90/90). Legacy unchanged.

* fix(next): guard jumpToMeasurement camera-centering for native viewports

jumpToMeasurement re-centers the camera when a measurement is off-screen via
isMeasurementWithinViewport (calls viewport.getCamera()) + getCamera/setCamera.
Native PLANAR_NEXT viewports have neither, so jumping to a measurement threw
'viewport.getCamera is not a function' at the gate before the centering block.

Short-circuit the centering on native (isGenericViewport) so it is skipped;
setViewReference above already navigated to the measurement's slice, so the
measurement is still reached - only in-plane re-centering is deferred.
TODO(next): port in-plane centering via the camera bridge + setViewState pan.

Verified live: native viewport has no getCamera (getCamera() throws) and
isGenericViewport is true, so the throwing branch is now skipped; setViewReference
remains available for slice navigation. Legacy unchanged.

* fix(next): make getViewportAlignmentData + updateViewport native-safe

Two CornerstoneViewportService methods called getCamera()/setCamera(), which
direct PLANAR_NEXT (next) viewports lack:
- getViewportAlignmentData looped every viewport reading getCamera().viewPlaneNormal
  (reached from findNavigationCompatibleViewportId on a cross-orientation
  jumpToMeasurement). Native reads viewPlaneNormal from getViewReference() instead
  (both backends populate it); legacy keeps getCamera (byte-identical).
- updateViewport (metadata-invalidation re-mount, keepCamera) read getCamera()
  unconditionally and had no native branch in its stack/volume if/else, so it threw
  and would not re-mount native data. Add a native branch that snapshots/restores the
  camera via the view-state bridges and routes the re-mount through _setDisplaySets
  (backend.dispatchMount, which dispatches by data shape). Legacy path unchanged.

Verified live on native: getViewportAlignmentData returns data (no throw);
updateViewport(keepCamera) re-mounts, restores zoom (1.4/1.5), keeps the image actor
and renders, with zero errors.

* refactor(next): move viewport interaction ops into a Legacy/Next operations backend

The commandsModule carried inline native-vs-legacy branches for every viewport
interaction/appearance command. Extract them into a dedicated operations backend
that mirrors the existing IViewportBackend twin pattern:
- IViewportOperations: the interface (flip/invert/rotate/reset/scaleBy/
  setWindowLevel/setColormap/getViewPlaneNormal/centerOnMeasurement + 3D VR ops)
- LegacyViewportOperations: legacy lane via direct legacy APIs (getCamera/
  setProperties/getViewPresentation/resetCamera/actors), lifted verbatim
- NextViewportOperations: native lane via the presentation/camera-state bridges +
  native semantic API (getViewState/resetViewState/getViewReference/setZoom); the
  3D VR ops warn-once and no-op behind a CS-14 gate (native VR not supported yet)
- viewportOperations: per-viewport dispatcher (isGenericViewport ? next : legacy)

commandsModule's 13 interaction commands become one-line delegations (the file
shrinks ~239 lines) and CornerstoneViewportService.getViewportAlignmentData uses
viewportOperations.getViewPlaneNormal.

Dispatch is per-viewport (not flag-selected like the lifecycle IViewportBackend)
because operations run on already-created, self-describing viewports and a session
can mix lanes; this preserves the previous inline isGenericViewport branching
exactly. Render() stays in the command (per-command render timing preserved).

Validated live both lanes: native (flag on) applies all ops (invert/flip/rotate/
zoom/WL/colormap; reset is camera-only) and legacy (flag off) is byte-identical
(parallelScale*0.9 zoom, getViewPresentation rotation, resetProperties+resetCamera),
both with a clean console. Adversarial review found no byte-identity/runtime defects.
Segmentation untouched.

* feat(next): render native 3D volume rendering + enable its VR operations

Make native VOLUME_3D_NEXT viewports actually render volume rendering and wire up
the 3D VR operations:

- CornerstoneViewportService._setNativeVolumeDisplaySets: a 3D viewport (cornerstone
  type VOLUME_3D_NEXT) now mounts with setDisplaySets({ options: { renderMode:
  'vtkVolume3d' } }) instead of the planar { orientation, role }, and applies the
  display-set's volume-rendering preset to the volume actor via csUtils.applyPreset
  (the bare native VolumeViewport3D has no setProperties). colormap (a planar LUT
  concept) is skipped for 3D. The dataId registration is unchanged (the volume3d data
  provider reads imageIds/volumeId and ignores the stored kind).

- NextViewportOperations: the four VR ops are no longer CS-14 no-ops. setPreset
  applies the preset to the volume actor via applyPreset; setVolumeRenderingQuality/
  shiftVolumeOpacityPoints/setVolumeLighting operate on the vtk volume actor through
  getActors (which native VolumeViewport3D exposes), so they reuse the legacy
  actor-based implementations. Pairs with the cornerstone fix that keeps the 3D
  viewport's canvas visible.

Verified live: native 3D VR renders (CT-Bone/CT-Cardiac presets) and all four VR
commands apply on a native VOLUME_3D_NEXT viewport; legacy VOLUME_3D unchanged.

* docs(next): refresh migration plan to HEAD (2026-06-19 audit)

* fix(next): target fusion colormap at the overlay binding

NextViewportOperations.setColormap dropped params.displaySetInstanceUID and
always wrote to the active source binding via getSourceDataId(), so a PT/CT
fusion colormap landed on the CT source instead of the PT overlay. Thread the
displaySetInstanceUID through to setViewportProperties so it targets the right
native binding (OHIF maps each display set 1:1 onto its bare dataId); falls back
to the source when no id is given (single-volume / plain stack colormap).

Also document DataIdRegistry.dataIdFor's 'overlay' suffix as reserved for the
same-UID source/overlay case (derived labelmap overlays, M4) rather than fusion,
whose distinct-UID overlays are already collision-free under the bare id.

* fix(next): make residual native-unsafe viewport sites safe

Sweeps the OHIF-side sites a native PLANAR_NEXT viewport reaches that still
called legacy-only APIs (getProperties/getCamera) or branched on the collapsed
viewportType:

- CornerstoneCacheService: persist the legacy stack/volume decision as
  viewportData.dataShapeType (createViewportData) and branch on it in
  invalidateViewportData instead of viewportType. Native collapses stack+volume
  onto PLANAR_NEXT, so a native stack previously fell through to the VOLUME
  rebuild and re-mounted as volume data on metadata invalidation. Falls back to
  viewportType for legacy/older data (byte-identical off-path).
- ViewportOrientationMarkers: gate the synthetic-IOP default-cosine check on
  dataShapeType, not viewportType==='stack' (dead on native -> guard was skipped).
- CornerstoneViewportDownloadForm: the capture viewport is the source's type, so a
  native source threw on getProperties/setStack/setProperties. Add a native capture
  path that re-mounts the source's already-registered dataId via setDisplaySets and
  copies presentation + view state through the bridges (legacy path byte-identical).
- tmtv ROI-threshold: read the slice focal point via a new getViewportFocalPoint
  bridge (native getViewReference().cameraFocalPoint vs legacy getCamera().focalPoint)
  instead of getCamera(), which is absent on native.

New bridge getViewportFocalPoint added to getViewportPresentation.ts and exported
from @ohif/extension-cornerstone. Validated live: native stack renders, console
clean, viewportData.dataShapeType='stack' while viewportType='planarNext',
orientation markers render.

* feat(next): mount native video/WSI/ECG viewports

Under useNextViewports, NextViewportBackend.dispatchMount routed ALL viewports by
data shape (volume vs stack), so VIDEO_NEXT/WHOLE_SLIDE_NEXT/ECG_NEXT constructed
as native classes but mis-ran _setStackViewport's stack-specific prefetch/VOI/
kind:'planar' logic and never reached their dedicated mounts; ECG additionally
called the absent setEcg.

- NextViewportBackend.dispatchMount: route the non-planar families by viewport
  type to _setEcgViewport / _setOtherViewport (mirrors the legacy backend's type
  dispatch); planar stack/volume still routes by data shape.
- _setEcgViewport: native branch registers {kind:'ecg', sourceDataId} and mounts
  via the generic setDisplaySets API (native ECG has no setEcg).
- _setOtherViewport: native branch registers {kind:'video', sourceDataId} or, for
  WSI, {kind:'wsi', imageIds, options:{webClient}} with the client resolved from
  WADO_WEB_CLIENT metadata exactly as the legacy WSI adapter does, then mounts via
  setDisplaySets + setViewReference.
- DataIdPayload widened to a family-specific union (planar/video/ecg/wsi).

All registration goes through the ref-counted DataIdRegistry (§4.7). OHIF-only —
the cornerstone native classes already support setDisplaySets (per the
genericVideo/genericEcg/genericWsi examples). Validated: native planar render
unaffected by the dispatch change (console clean); video/WSI/ECG mounts follow the
canonical cornerstone examples but are not yet live-validated (no such study on the
dev dicomweb).

* chore(next): guard the useNextViewports flag-read allowlist (M7 prep)

Adds .scripts/check-next-viewports-flag-reads.mjs (wired as `yarn
next:check-flag-reads`) enforcing migration plan §4.2: the flag may be read only
in the sanctioned seam — getCornerstoneViewportType (type selection),
CornerstoneViewportService (backend selection), nextViewports.ts (the accessor),
init.tsx (the one appConfig.useNextViewports read), and the TEMP dev toggle in
getToolbarModule.tsx. Any other isNextViewportsEnabled()/appConfig.useNextViewports
read under extensions/cornerstone/src fails the check, so the legacy off-path
cannot drift. (Comment-only mentions are ignored; tests are exempt.)

The earlier audit framed the '2 sanctioned reads' contract as already violated by a
'backend trio', but those files only MENTION the flag in doc comments — the actual
runtime read surface is the sanctioned set above, so the rule is enforceable as
written. TODO_BEFORE_MERGE.md updated: the guard is permanent (not a dev revert),
and removing the dev toggle must also drop its allowlist entry.

Does NOT perform the destructive M7 reverts (config default flip, toggle button):
those are premature while segmentation/M4 is unmigrated and would disable the
in-browser test loop.

* docs(next): mark CS-12 native calibration done; refresh CS-20/M6 status

* docs(next): re-verify migration status at HEAD; correct stale prose

Re-audited every milestone (M0-M7) and CS blocker against HEAD source via a
multi-agent audit + adversarial verification pass. Five commits landed after the
last full prose refresh (05e0df0ca) and only d5d03d888 touched the doc, so the
Implementation status section was materially stale. Corrections:

- M2: fusion colormap keying is FIXED (7b61e08ee), not 'unsound'
- M3: four 'native-unsafe throws' are FIXED (a19bd7826); the per-volume WL panel
  is guarded (d28202610), not throwing - feature port, not a crash
- M6: video/WSI/ECG ARE mounted natively (ca746e2f0)
- M7: flag-read allowlist IS built (b5784ca80), just not wired into CI
- CS-21: stated trigger is unreachable; narrowed to single-point SCOORD3D, and
  the open code is PlanarViewReferenceController.ts (not planarViewReference.ts)

Adds a consolidated, verified remaining-work punch-list and a corrections
subsection. The one real native crash that remains is the M4 segmentation OHIF
half (convertStackToVolumeViewport throws AND promotes to legacy ORTHOGRAPHIC).

* Refactor SegmentationService to support dual backends for segmentation handling

- Introduced ISegmentationBackend interface to define methods for segmentation backends.
- Implemented LegacySegmentationBackend for existing behavior with stack/volume promotion.
- Implemented NextSegmentationBackend for native GenericViewport handling without promotion.
- Updated SegmentationService to utilize the appropriate backend based on viewport type.
- Removed legacy viewport handling logic from SegmentationService and delegated to backends.
- Enhanced segmentation data assembly to support overlapping segments in the Next backend.
- Updated CornerstoneViewportService to ensure proper restoration of segmentation presentations.
- Added support for preserving additional query parameters in the application.

* temp

* Refactor CornerstoneViewportService to optimize setDisplaySets handling for 3D volumes

* d

* Refactor viewport handling and opacity management for native volume rendering

* fix(WindowLevel): re-sync fusion tab to foreground default after async resolve

The effect only adopted the foreground (PT) default when activeDisplaySetUID was
falsy, so if foregroundDisplaySets was empty at mount the tab seeded to the CT
fallback and stayed pinned to CT once PT resolved. Track explicit user selection
and re-sync to the foreground default until the user picks a tab.

* chore(next): remove WIP migration plan artifacts from the branch

Delete the planning docs and plan-viewer pages that were committed during
development (migration plans, blueprint, TODO_BEFORE_MERGE, plan-viewer.html).
They are not part of the shipped viewer and only attract review noise.

* fix(next): address review findings (guards + correctness)

Apply CodeRabbit review comments on the migration code:
- commandsModule: guard missing viewport before setWindowLevel/render
- WindowLevel: drop stale activeDisplaySetUID when the viewport's display sets change
- SegmentationService: hard guard when segmentation lookup fails before backend classify
- LegacyViewportOperations: guard getActors()[0] before actor-chain calls
- CornerstoneViewportService: guard empty native stack imageIds; don't route generic
  overlay-only mounts to legacy setVolumes; stop overwriting tracked display sets with
  base-volume-only ids (drops SEG/RT/fusion overlay UIDs)
- getCornerstoneViewportType: list orthographic/volume3d in the invalid-type error
- nextViewports: skip reload (warn) when the toggle can't be persisted
- tmtv: guard missing focal point before mutating ROI annotation coordinates
- check-next-viewports-flag-reads: also catch bracket/destructured flag reads

* fix(overlay): show instance number on next viewports

The viewport overlay's getInstanceNumber switched on viewportData.viewportType,
which for next viewports is the native PLANAR_NEXT type (the stack/volume shape
is persisted separately as dataShapeType). The switch matched no case, so the
instance number was null and the overlay showed only the slice index/count.

Switch on (dataShapeType ?? viewportType) so next stack/volume viewports take
the correct branch (legacy is unaffected). Also make _getInstanceNumberFromVolume
read the view-plane normal via getViewReference for native viewports, which
expose no getCamera, so routing next volume viewports through it cannot throw.

* fix(overlay): refresh window level on series change for next viewports

The overlay's WW/WL comes from useViewportRendering, whose init effect reads the
VOI via getViewportProperties. On series change the effect re-runs, but native
(next) viewports expose only explicit VOI overrides through
getDisplaySetPresentation; a freshly shown series has none, so properties.voiRange
was undefined, setVoiRange was skipped, and the overlay kept showing the previous
series' window level. Legacy getProperties always returns the applied VOI, so only
native viewports were affected.

Fall back to the viewport's computed default VOI (getDefaultVOIRange) for generic
viewports when no override is stored, matching the LivewireContourTool and
WindowLevelTool bridges. Legacy behavior is unchanged.

* fix(scrollbar): seed slice state on orientation change for next viewports

The progress scrollbar seeded its slice state (imageIndex/numberOfSlices) only
when viewportData changed. Native (next) viewports keep the same viewportData
across a stack->volume transition or an orientation change, and the slice-
navigation event does not fire until the first scroll, so the scrollbar was
missing on the initial slice (or stale with a wrong slice count) until the user
scrolled once.

Re-seed the slice state from the live viewport on CAMERA_MODIFIED (which native
viewports emit on orientation/geometry changes, as the sibling full-mode hook
already relies on). A guard skips redundant state updates so pure pan/zoom does
not churn React state.

* refactor(next): call resetDisplaySetPresentation on reset

Follow the cs3D rename of the native viewport's presentation-reset method from
resetProperties to resetDisplaySetPresentation (the next viewport API uses
get/set DisplaySetPresentation, not get/set Properties). Behavior unchanged.

* fix(segmentation): make border/outline thickness slider integer-only

The Border (outline width) slider for labelmap and RTSTRUCT used step=0.1,
allowing fractional outline widths. Outline thickness is a pixel width and
should be a whole number, so use step=1 and round the committed value. The
fill/opacity sliders keep their fractional step.

* fix(crosshairs): guard resetCrosshairs against unregistered Crosshairs tool

resetCrosshairs (run by Reset Viewport) called toolGroup.getToolInstance('Crosshairs')
for every tool group; getToolInstance logs 'Crosshairs is not registered with this
toolGroup' when the tool is absent, and a next viewport's default tool group does not
include Crosshairs, so Reset Viewport logged a spurious warning. Guard the lookup with
toolGroup.hasTool('Crosshairs') and skip tool groups that lack it; also guard against a
missing tool group for the viewport.

* fix(next-fusion): promote source to volume slice when a data overlay is added

A data overlay (fusion) on a next (PLANAR_NEXT) viewport rendered the source as a
vtkImage stack while the overlay was a vtkVolumeSlice, producing a broken/unstable
fusion (geometry mismatch, intermittent across slices/scroll). Two causes:

1. CornerstoneCacheService.createViewportData built stack data when the fusion's
   primary display set resolved to a stack shape, so the source never got a
   volumeId. Force a volume (orthographic) shape when there are 2+ reconstructable
   image display sets (a data fusion must be volume; legacy already did this, and
   non-reconstructable SEG/RT overlays are excluded).

2. dataIdRegistry.register used first-writer-wins, so re-registering the source
   (originally a vtkImage stack, no volumeId) with its fusion volumeId was dropped,
   leaving its dataset volumeId-less -> the render-path decision kept it vtkImage.
   Update the provider when a payload promotes a dataId to volume-backed.

Validated via agent-browser: adding a PT overlay onto a CT source now mounts both
as vtkVolumeSlice (mode=volume), the fusion is anatomically coherent and stable
across scroll, with no console errors.

* fix(next-rtss): keep referenced CT in stack mode on RTSTRUCT hydrate

RTSTRUCT (contour) hydration on a native PLANAR_NEXT viewport re-mounted the
referenced CT as a volume slice, which is the slow path the perf AC forbids.

Spike proved cs3d already renders contour segmentations on a stack/vtkImage
PLANAR_NEXT viewport (via the annotation + isReferenceViewable path), and that
stack-mode contour scroll is fast (~0.15ms/scroll, no metadata storm) once the
canvas-dimension layout-thrash fix is in place. The only remaining issue was the
hydrate re-mount promoting the CT to volume.

Pin the referenced viewport to 'stack' for RTSTRUCT hydration on next viewports:
hydrateSecondaryDisplaySet passes viewportType:'stack' (scoped to RTSTRUCT +
isNextViewportsEnabled), and loadSegmentationDisplaySetsForViewport applies it as
a per-mount viewportOptions override. SEG and legacy keep their current behavior.

Verified at runtime: after hydrate the viewport stays vtkImage/stack, contours
render across slices, scroll is 0.16ms with 0 metaData.get calls, CT VOI intact.

* fix(next-fusion): match legacy initial data-overlay opacity (~40%) on next viewports

The data-overlay add path passes a nominal colormap opacity of 0.9. Legacy
volume rendering attenuates that to ~40% effective via ray-cast opacity-unit-
distance correction, but native PLANAR_NEXT viewports composite the overlay as
a flat 2D image-slice blend with no such attenuation, so the same 0.9 rendered
at ~80-90%. Override the initial overlay opacity to 0.4 for next viewports
(mirrors the TMTV fusion NEXT_FUSION_PT_OPACITY), gated on isGenericViewport and
a numeric opacity so SEG/RTSTRUCT overlays and the legacy path are unaffected.

* fix(next-fusion): preserve fusion on orientation change in next viewports

The orientation corner menu branched on viewportType === ORTHOGRAPHIC to decide
in-place reorient vs viewport recreation. Native next viewports always report
planarNext, so a fusion already in volume mode wrongly took the recreation path,
which passes empty displaySetOptions and drops the PET overlay colormap/opacity
(rendering PET only). Branch instead on whether the live viewport is already in
volume mode (isOrthographicViewportType || utilities.viewportIsInVolumeMode):
volume-mode viewports reorient in place (setViewportOrientation, which preserves
all bindings and their presentation); only a genuine stack->volume conversion
recreates. Legacy ortho/stack behavior is unchanged.

* fix(next-seg): preserve base image window level through SEG hydration

Hydrating a SEG re-mounted the referenced image and restored a stale, computed
VOI from the LUT presentation store, brightening the base image (e.g. an MR went
from its DICOM WC/WW default to a volume min/max default). During the SEG-load
intermediate mount the base image briefly carries a computed default VOI; the
native read bridge returned it with no isComputedVOI marker, so cleanProperties
never stripped it and it was persisted then replayed over the correct default.
Legacy StackViewport tags computed VOIs (isComputedVOI) so they are stripped.
Mirror that: in getViewportProperties, stamp isComputedVOI on a native binding's
VOI when it matches the binding's getDefaultVOIRange, so the LUT capture strips
it. Harmless when a genuine user VOI equals the default (stripping falls back to
the same value).

* fix(next-mpr): re-seed slice scrollbar after post-mount camera carry

On a stack->volume/MPR transition the slice carry (e.g. layout-selector MPR HP
restoring the prior slice) moves the camera and fires its slice events
synchronously during the mount, before the scrollbar effect attaches its
listeners and around its initial seed -- so the scrollbar latched the mount-time
index. Re-seed once on the next frame after mount+carry settle; the pushSliceData
guard makes it a no-op when nothing changed (no churn).

* chore(deps): bump @cornerstonejs/* to 5.1.2

* chore: empty commit

* fix(next): use published genericViewportDisplaySetMetadataProvider export

The next viewport backend imported genericViewportDataSetMetadataProvider
from @cornerstonejs/core, a symbol that only existed in the local custom
cornerstone worktree (linked via symlink). Published cornerstone exports it
as genericViewportDisplaySetMetadataProvider (same add/remove/get/clear API).

CI always builds against published packages, so the production rspack build
failed (ESModulesLinkingError -> Netlify red) and the cypress dev-server
build showed a full-screen error overlay that blocked all clicks (PR_CHECKS
red). Renaming to the published symbol fixes both.

* test(e2e): extend Scoord3dProbe jump screenshot retry window

The jump-to-measurement screenshot was the only failing assertion (pre/post
hydration pass). It uses waitVolumeLoad:false, so on slower CI the dynamic
tfl_dyn_fast_tra series can still be progressively loading when the shot is
taken, giving a partial probe value (52.0 vs baseline 78.0) and shifted VOI.
Raise checkForScreenshot attempts 10->20 and delay 1250->2000ms (~11s -> ~40s)
to let the volume settle before failing.

* fix(next): address review findings (scaleBy 3D crash, fusion W/L target, off-path gate, seg export perf)

- NextViewportOperations.scaleBy: guard getZoom/setZoom so the zoom hotkey
  no-ops on a native VolumeViewport3D instead of throwing (matches legacy).
- Native setWindowLevel: forward displaySetInstanceUID so PT/CT fusion W/L
  targets the intended binding (mirrors setColormap) instead of always source.
- CornerstoneCacheService: scope the reconstructable-fusion STACK->ORTHOGRAPHIC
  promotion to PLANAR_NEXT so the legacy (flag-off) path stays byte-identical.
- dicom-seg buildLabelmap3D: precompute a referencedImageId->index Map to drop
  the multi-layer export from O(slices^2) to O(slices).

* fix(next): address CodeRabbit review (dispatch discriminator, seg return contract, error msg)

- NextViewportBackend.dispatchMount: route stack/volume on the persisted
  dataShapeType contract instead of the lazily-populated 'volume' field probe.
- SegmentationService.attemptStackToVolumeConversion: return false explicitly on
  the frame-of-reference-mismatch path to honor the Promise<boolean> contract.
- getCornerstoneViewportType: list orthographic/volume3d in the legacy-path
  invalid-type error (both are valid and handled above the throw).

* test(next): update getCornerstoneViewportType invalid-type assertion

Match the legacy-path error message now listing orthographic/volume3d (b16129ece).

* chore(next): replace dev toggle + flag-read guard with URL opt-in

- Remove the .scripts/check-next-viewports-flag-reads.mjs CI guard and its
  package.json script entry.
- Drop the TEMP ToggleNextViewport toolbar button across all modes plus the
  toggleNextViewports command and getToolbarModule evaluator (revert mode files
  to their master state; keep the real native-path toolbar fixes).
- Remove the localStorage override / toggleNextViewportsAndReload from
  nextViewports.ts; resolveNextViewportsEnabled now honors a ?useNextViewports
  URL query param (true/1/empty enable) over appConfig.
- Stop forcing useNextViewports:true in default.js (opt-in via URL/appConfig).
- Preserve useNextViewports across navigation (preserveQueryParameters).

* fix(tmtv): keep legacy fusion PT opacity ramp; flatten only on next path

Flattening the fusion PT opacity to a scalar 0.9 in hpViewports changed the
legacy TMTV fusion rendering (the ramp keeps low PT values transparent so the CT
shows through). Restore the legacy ramp in hpViewports and instead replace it
with the flat native scalar (0.4) inside getHangingProtocolModule only when
useNextViewports is on, so legacy is unchanged and native still gets a flat blend.

* redo

* fix(next): avoid top-level dicomWebUtils destructure crash on boot

getSopClassHandlerModule destructured transferDenaturalizedDataset /
fixMultiValueKeys from dicomWebUtils at module-eval time. This module and
@ohif/extension-default form a circular import, so dicomWebUtils can be
undefined at eval time depending on bundler module order; the top-level
destructure then throws (Cannot destructure property ... of dicomWebUtils
as it is undefined) and crashes app boot before the Layout renders, which
surfaced as the Playwright globalSetup warmup timing out on [data-cy=Layout].

Access the utils lazily at call time inside getDICOMwebMetadata instead.

* fix(next): guard remount no-op path and defer legacy camera snapshot

- CornerstoneViewportService.updateViewport: backend.remount() is typed
  Promise<void> | undefined and returns undefined for viewport families with
  no re-mount path; guard before .then() so those families no longer throw.
- LegacyViewportBackend.remount: take the camera snapshot inside the volume
  branch (the only consumer) instead of before the family checks, so families
  without a camera surface no-op safely and the stack path skips a dead call.

Addresses CodeRabbit review findings on PR #6101.

* feat(next): add ?cpu=true URL opt-in to force the CPU render path

Mirrors the ?useNextViewports opt-in: a cpu URL query param overrides
appConfig.useCPURendering per-session (?cpu, =true, or =1 enable it).
Wired through cornerstone.setUseCPURendering in init, whose global flag is
consulted by the GenericViewport PlanarRenderPathDecisionService for both
the image (CPU_IMAGE) and volume (CPU_VOLUME) paths, so under
useNextViewports a single ?cpu=true forces the next viewport onto CPU.

Extracted the shared query-param parsing into resolveBooleanUrlOptIn.

* feat(cornerstone): select render backends via viewportRendering param

Replaces the boolean cpu URL param with viewportRendering=cpu|webgl|auto
(or any backend registered via cornerstone registerRenderBackend, e.g. a
webgpu backend), mapped to the cornerstone render-backend registry. A
per-viewport-type override (e.g. orthographic.viewportRendering=cpu) is
passed as the per-mount renderBackend option on native planar mounts.
The global value also drives the legacy useCPURendering flag so legacy
viewports follow the same selection, letting a session force GPU when
the deployed config defaults to CPU.

* fix(dicom-seg): store overlapping segmentations as binary SEG

A LABELMAP SEG frame stores a single label per voxel, so the labelmap
encoder cannot represent overlapping segments (the later layer wins).
When the export produces multiple overlapping layers and the resolved
store mode is labelmap, switch that store to the binary SEG encoding,
which writes overlapping segments as separate frames referencing the
same source slice.

* fix(cornerstone): apply review fixes to viewport backends

- Anchor the cached-volume lookup in NextViewportAdapter to the
  loaderSchema:displaySetInstanceUID id shape instead of a substring
  match, so a derived volume id embedding the same UID cannot resolve.
- Keep the viewing orientation on fitViewportToWindow (scaleBy 0) for
  native planar viewports, matching legacy resetCamera semantics.
- Release legacy WSI metadata-provider registrations through the same
  ref-counted DataIdRegistry the native backend uses, so entries are
  removed on viewport disable and service destroy.

* test(cornerstone): use real volumeId shape in adapter contract test

The anchored cached-volume lookup rejects ids that merely embed the
display set UID, so the mock now uses the real
volumeLoaderSchema:displaySetInstanceUID shape and asserts an embedded
UID id does not match.

* docs(config): document useNextViewports and viewportRendering in default config

Gives deployments an obvious place to opt into the native Generic
Viewport path and configure its render backend. Both stay off/auto by
default; the explicit false preserves the opt-in contract.

* refactor(config): group next viewport settings under genericViewports

Replaces the two top-level app config keys (useNextViewports,
viewportRendering) with one genericViewports object:
{ enabled, viewportRendering }. The URL params are unchanged.

* chore(deps): bump cornerstone packages to 5.4.15

Picks up the planar initial-slice remap fix (cornerstonejs/cornerstone3D#2799):
opening a SEG display set now lands on the first segmented slice instead of
its mirror when the cached volume reverses the imageId ordering.

* test(segmentation): verify overlapping SEG rendering

* fix(sr): make SCOORD3D hydration deterministic

---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2026-07-11 12:13:54 -04:00
ohif-bot
00bb77d940 chore(version): Update package versions to 3.13.0-beta.122 [skip ci] 2026-07-11 06:24:30 +00:00
Ghadeer Albattarni
01939e2236
test: add E2E test for contour combine intersect and subtract operations (#6131) 2026-07-11 02:21:39 -04:00
ohif-bot
f0e2f64397 chore(version): Update package versions to 3.13.0-beta.121 [skip ci] 2026-07-10 22:54:59 +00:00
Alireza
43226d9191
fix(app): appearance modal provider scope, worklist preview persistence, and tag browser label overflow (#6136)
- Insert ServiceProvidersManager providers ahead of the dialog/modal
  providers in App.tsx: modal content renders as a sibling of the
  provider's children, so contexts registered via the manager (e.g.
  ActiveThemeProvider) were out of scope and the appearance modal
  crashed with 'useActiveTheme must be used within an ActiveThemeProvider'.
- Persist the worklist preview panel open/closed state in
  sessionStorage so it survives navigating into a study and back.
- Keep the DICOM tag browser instance number label on one line:
  the words truncate, the (n of total) digits never clip.
2026-07-10 18:52:06 -04:00
ohif-bot
dc9df56be4 chore(version): Update package versions to 3.13.0-beta.120 [skip ci] 2026-07-10 21:08:34 +00:00
Alireza
9c76afa075
feat(segmentation): replace One Click Segment with ClickSegmentTool (#6135)
* feat(segmentation): replace One Click Segment with ClickSegmentTool

Wire OHIF to ClickSegmentTool from Cornerstone3D (#2780), rename the
toolbox button to Click to Segment, and enable it only on PET (PT)
viewports.

* chore(deps): bump @cornerstonejs packages to 5.4.13

Pick up ClickSegmentTool from the published Cornerstone3D release so
OHIF installs the tool from npm without a local CS3D link.
2026-07-10 17:05:26 -04:00
ohif-bot
6d8a275171 chore(version): Update package versions to 3.13.0-beta.119 [skip ci] 2026-07-10 19:38:19 +00:00
Alireza
1eee3e6119
fix(MetadataProvider): Correctly assign imageId for multiframe images and remove unused frame information retrieval method (#5965) 2026-07-10 15:35:09 -04:00
ohif-bot
2c54616136 chore(version): Update package versions to 3.13.0-beta.118 [skip ci] 2026-07-10 17:34:50 +00:00
Alireza
9329dd5b55
test(core): use fake timers in Queue unit tests to stop CI flakes (#6134)
The Queue tests measured real setTimeout wall-clock time and asserted
elapsed < 2 * threshold. On busy CI runners a 2400ms timer can take
longer than 4800ms to fire, failing the assertion intermittently (seen
in downstream validation runs). Switch to jest's modern fake timers so
elapsed is exactly the timeout delay: the tests are deterministic and
no longer spend ~5 seconds of real time waiting.
2026-07-10 13:31:42 -04:00
ohif-bot
75105ced04 chore(version): Update package versions to 3.13.0-beta.117 [skip ci] 2026-07-10 16:46:10 +00:00
Bill Wallace
b266c0a86a
feat: Add extensibility for tmtv and segmentation modes (#6128)
* feat: Add extensibility for tmtv and segmentation modes

* Fixes for ordering issues on laod

* Remove unnecessary reference lookup

* Chane side panel timing to fix tests

* PR comments - change how mode definitions get created

* Improvements to mode customizations

* Start organizing customizations

* Misc fixes for a customization demo page

* Security fixes

* PR requested changes to naming
2026-07-10 12:43:17 -04:00
ohif-bot
f79055f98e chore(version): Update package versions to 3.13.0-beta.116 [skip ci] 2026-07-10 12:19:06 +00:00
Bill Wallace
6b6761088b
fix: Several new worklist issues (#6130)
* fix: Several new worklist issues

* refactor: Export a single OnStudyDoubleClick type from the StudyList barrel

Addresses PR review feedback: the double-click handler signature was
written out in both TableProps and the WorkList customization cast,
so the two could drift apart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: Run worklist study double-click as a command, registrable by modes

- Modes can now export getCommandsModule on their definition; appInit
  registers it (via the new ExtensionManager.registerCommandsModule)
  before the mode is instantiated, in a new 'WORKLIST' commands context,
  so the commands are available on the worklist before any mode route
  is entered.
- The workList.onStudyDoubleClick customization is now a command run
  input (name/options) instead of a bare function, defaulting to the
  new launchDefaultMode command, which launches the default workflow
  falling back to the first applicable one. commandOptions.workflowId
  overrides it to a specific mode.
- Duplicate mode ids are now skipped before running their modeFactory
  rather than after.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 08:16:16 -04:00
Bill Wallace
dbb8b1526e
feat: Add support for labelmap seg images in any supported tsuid (also for compressed bitmap) (#5806)
* feat: Update to use pnpm (#6031) [simulated squash-merge]

* feat: load DICOM SEG images via imageLoader

* feat: load DICOM SEG images via imageLoader

* PR review fixes

* Test fragment of compressed multiframes

* fix: Removed dependencies incorrectly

* Update frame as a targetted change to avoid stripping remaining params

* lock

* Update test to agree with the missing representation branch

* test(contour): contour color change coverage (#6042)

* test(contour): contour interactions delete segment (#6069)

---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>

* chore(version): Update package versions to 3.13.0-beta.98 [skip ci]

* feat(testing): OHIF Test Agent Skills (#5993)

* chore(version): Update package versions to 3.13.0-beta.99 [skip ci]

* test(contour): Add the ContourSegmentToggleLock.spec.ts test file to test contour locking (#6072)

* chore(version): Update package versions to 3.13.0-beta.100 [skip ci]

* chore(testing): Flock lock for playwright tests; Pin node version for OHIF; add more workers (#6099)

* update Cypress apt deps for Ubuntu Noble (drop libgconf-2-4, libasound2→libasound2t64)

* chore(version): Update package versions to 3.13.0-beta.101 [skip ci]

* Debug fixes

* perf(seg): pass explicit frame decode concurrency (16) to SEG loader

Pass an explicit concurrency value (SEG_FRAME_DECODE_CONCURRENCY = 16) into
createFromDicomSegImageId rather than relying on the adapter default, so the
SEG frame fetch/decode parallelism is set at the call site and is ready to
become configurable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(behaviours): add behaviours section + multiframe Part 10 prefetch proposal

Start a "Behaviours" docs section for documenting how the system and UI behave
end-to-end (observed behaviours, design proposals, and failure modes), with an
index README and a Docusaurus category.

First entry is the proposal for loading a multiframe SEG as a single Part 10
instance: prefetch the whole instance (gated by loadMultiframeAsPart10RaceTimeMs),
parse it with dcmjs (handling multipart/related vs raw DICOM), and register the
per-frame compressed pixels into the Cornerstone3D core image cache (the single
uniform frame registry) so the per-frame load path is served locally while the
standard decode path is unchanged. Best-effort: falls back to per-frame fetches
on any failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Pre-cache the image data using a full part10 file for performance

* Add customizations to specify type of segmentation save

* Add clearcache of the cacheData

* Bump @cornerstonejs/* pins 5.1.3 -> 5.4.10 to match libs/@cornerstonejs base

libs/@cornerstonejs (fix/use-imageLoader-for-seg) is based on the released
cs3d 5.4.10 (merge-base with origin/main is the 5.4.10 version bump), so pin
OHIF to that release. The local branch changes still reach the app via the
cs3d:link symlinks; these pins keep the lockfile/npm fallback aligned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix seg OOM

* fix: file meta garbage element, bogus NumberOfFrames, unguarded source-map-loader

- dicomWriter: drop the naturalized meta.TransferSyntaxUID assignment that
  dcmjs wrote as a garbage (0000,0000) element into every saved file's meta
  group (download / clipboard / local wadouri blob / local store); keep the
  hex 00020010 assignment, which is required for string-form _meta fallbacks.
- registerNaturalizedDatasetForLocalWadouri: keep the computed frame count
  local so single-frame IODs (SR, RTSTRUCT) no longer gain a bogus
  NumberOfFrames element in their serialized form.
- rsbuild.config: resolve source-map-loader opportunistically (it is not a
  project dependency; the rule only serves the gitignored cs3d-link
  workflow) so fresh clones no longer crash at config load.
- Regression tests for both writer fixes (mutation-checked).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* PR review comment fixes

* Update versions

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: diattamo <mmddiatta@gmail.com>
Co-authored-by: ohif-bot <danny.ri.brown+ohif-bot@gmail.com>
Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
2026-07-09 20:54:23 -04:00
ohif-bot
b996070583 chore(version): Update package versions to 3.13.0-beta.115 [skip ci] 2026-07-08 18:15:05 +00:00
Bill Wallace
1b6fa2194e
Revert "feat: Add extensibility for tmtv and segmentation modes"
This reverts commit 84c7d17b13.
2026-07-08 14:11:24 -04:00
Bill Wallace
84c7d17b13
feat: Add extensibility for tmtv and segmentation modes 2026-07-08 14:08:59 -04:00
ohif-bot
76687cf496 chore(version): Update package versions to 3.13.0-beta.114 [skip ci] 2026-07-07 19:33:37 +00:00
Bill Wallace
3dd5c70cb2
feat: Add customization URL parameter (#5992)
* Add customization URL parameter

* fix: Preserve should be customizeable

* Update customizations docs

* fix: Overlay items on patient name

* Add customization test

* Fix resolve to absolute path

* fix: Warn on no data in load

* Remove unused customization stuff

* fix: PR comments

* Update stored parameters to only use an array for mulitples

* Remove requires ohif.* special call out

* Remove strict mode

* PR comments

* Document segmentation examples

* Add three examples as requested

* PR comments

* lock

* Remove old customizatoin export

* fix: Ordering issues on customization loads

* fix: Use correct default for dev builds app config

* Fixes for conflicts

* chore: restore pnpm-lock.yaml to match master

The lockfile diff was incidental peer-descriptor churn and carried no
functional dependency change. It tripped the CircleCI security-audit gate
(which only runs when pnpm-lock.yaml is in the PR diff), surfacing a
pre-existing critical `decompress` transitive vuln that also exists on
master. Restoring master's lockfile removes the audit trigger.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): restore json5 lockfile entry; ignore unfixable decompress GHSA

The previous commit restored pnpm-lock.yaml from master, which dropped the
json5@2.2.3 entry that platform/core legitimately depends on (JSONC parsing
for the customization feature). That broke `--frozen-lockfile` install
(ERR_PNPM_OUTDATED_LOCKFILE). This restores the correct lockfile.

Because the lockfile must change (json5), the CircleCI security-audit gate
runs and previously failed on a critical `decompress` <=4.2.1 zip-slip
advisory. This is a pre-existing transitive vuln (present on master too) with
no published patch — decompress's latest release is 4.2.1, so no version
bump/override can resolve it. It reaches the tree only via @itk-wasm/dam, a
build/data-asset extraction tool under @cornerstonejs/labelmap-interpolation.

Add GHSA-mp2f-45pm-3cg9 to the existing pnpm-workspace.yaml auditConfig
ignoreGhsas accepted-risk list, matching how the repo already exempts other
build-tooling advisories. `pnpm audit --audit-level high` now passes locally
(1 critical ignored, 0 high).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(e2e): fix visitStudy URL encoding that broke mpr2 study load

The visitStudy rewrite (added for the ?customization= option) built the URL
with new URLSearchParams({ StudyInstanceUIDs: studyInstanceUID }), which
percent-encodes the value. mpr2.spec.ts embeds an extra param in the UID
string ('<uid>&hangingprotocolid=mpr'), so the & and = were encoded and the
whole thing collapsed into one invalid StudyInstanceUIDs value -> the study
could not be found ('studies are not available'), the viewer never rendered,
and the side-panel-header-right click timed out.

Restore master's raw concatenation for StudyInstanceUIDs (so embedded params
survive as separate query params) while still appending the customization
option separately. Only mpr2 embeds & in the UID, matching the single failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* PR comments

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 15:30:27 -04:00
ohif-bot
3d9a17bc0c chore(version): Update package versions to 3.13.0-beta.113 [skip ci] 2026-07-07 18:01:19 +00:00
Joe Boccanfuso
333d73e334
fix(security): ignore decompress security vulnerability (#6125) 2026-07-07 13:58:15 -04:00
ohif-bot
e381d22200 chore(version): Update package versions to 3.13.0-beta.112 [skip ci] 2026-07-07 17:21:45 +00:00
Alireza
f8546ce0e0
fix: recipe config cleanup, report-only CSP, logout redirect validation (#6124) 2026-07-07 13:18:46 -04:00
ohif-bot
973631b7e8 chore(version): Update package versions to 3.13.0-beta.111 [skip ci] 2026-07-06 13:38:47 +00:00
Alireza
e0f42d4f1d
fix(dicom-pdf): enhance PDF loading with authentication support
- move authenticated rendered media loading into the datasource\n- route DICOM video display sets through Cornerstone video viewports\n- add a 3.12 to 3.13 migration note for the removed DICOM-video viewport namespace
2026-07-06 09:35:26 -04:00
ohif-bot
2800f82e3c chore(version): Update package versions to 3.13.0-beta.110 [skip ci] 2026-07-02 13:00:56 +00:00
Joe Boccanfuso
38214f26de
fix(wsi): mount WSI/SM viewports via setDisplaySets (#6107) 2026-07-02 08:57:13 -04:00
ohif-bot
cb6cdafd24 chore(version): Update package versions to 3.13.0-beta.109 [skip ci] 2026-06-30 13:28:46 +00:00
Alireza
aa1ba771e0
fix(test): stabilize DICOM Tag Browser scrollbar screenshot (#6118) 2026-06-30 09:25:37 -04:00
ohif-bot
d4153f12b2 chore(version): Update package versions to 3.13.0-beta.108 [skip ci] 2026-06-29 16:58:29 +00:00
Alireza
71c50db9b5
fix(ci): deploy docs with Netlify CLI monorepo filter (#6115) 2026-06-29 12:55:01 -04:00
ohif-bot
4cdcd34a33 chore(version): Update package versions to 3.13.0-beta.107 [skip ci] 2026-06-29 16:42:34 +00:00
Dan Rukas
0dfd321326
feat(ui-next): Adds appearance dialog with theme presets and custom theme support (#6041) 2026-06-29 12:38:46 -04:00
ohif-bot
0fcebe05c6 chore(version): Update package versions to 3.13.0-beta.106 [skip ci] 2026-06-29 16:36:28 +00:00
Dan Rukas
ac45a9497f
docs(site): add ui-next component documentation and upgrade Docusaurus (#6102) 2026-06-29 12:33:16 -04:00
Dan Rukas
03b180f185
fix(colors): Replace hardcoded colors with theme tokens (#6040) 2026-06-29 12:32:40 -04:00
ohif-bot
e1bc625ab8 chore(version): Update package versions to 3.13.0-beta.105 [skip ci] 2026-06-26 12:23:19 +00:00
Joe Boccanfuso
661ecb5b2e
fix(hotkeyBindings): consolidate Escape hotkey behavior for contour drawing tools (#6104)
* fix(hotkeyBindings): consolidate Escape hotkey behavior for contour drawing tools

* PR feedback.

* PR feedback.
2026-06-26 08:20:22 -04:00
ohif-bot
b7c8b865dd chore(version): Update package versions to 3.13.0-beta.104 [skip ci] 2026-06-24 19:21:51 +00:00
nithin-trenser
ee165b4d8e
Fix: Segmentation Hide All fails when switching from 2D to 3D fourUp (#5995)
* Fix: Segmentation "Hide All" fails when switching from 2D to 3D four-up viewport

* Update based on the review comment on segmentation load in 3d fourup

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix: simplify the conditional logic

* fix: playwright test

* fix: playwright test

* fix: render timeout in test

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Belbin-GK <150322972+Belbin-GK@users.noreply.github.com>
Co-authored-by: Devu Jayalekshmi <devu.jayalekshmi@trenser.com>
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2026-06-24 15:18:16 -04:00
ohif-bot
589d6a1506 chore(version): Update package versions to 3.13.0-beta.103 [skip ci] 2026-06-24 15:08:56 +00:00
diattamo
95200ec6e8
test(segmentation): switch getSegmentCount to expect(rows).toHaveCount in panel specs (#6105) 2026-06-24 11:05:27 -04:00
ohif-bot
8f6947b75d chore(version): Update package versions to 3.13.0-beta.102 [skip ci] 2026-06-24 14:39:34 +00:00
diattamo
79cb2356e1
chore(testing): Playwright tests for contour segmentation create, update, delete interactions (#6091) 2026-06-24 10:36:16 -04:00
ohif-bot
20632f97cf chore(version): Update package versions to 3.13.0-beta.101 [skip ci] 2026-06-23 06:25:18 +00:00
Joe Boccanfuso
c205965f87
chore(testing): Flock lock for playwright tests; Pin node version for OHIF; add more workers (#6099)
* update Cypress apt deps for Ubuntu Noble (drop libgconf-2-4, libasound2→libasound2t64)
2026-06-23 02:21:18 -04:00
ohif-bot
75488298cd chore(version): Update package versions to 3.13.0-beta.100 [skip ci] 2026-06-23 05:56:18 +00:00
diattamo
b24f039735
test(contour): Add the ContourSegmentToggleLock.spec.ts test file to test contour locking (#6072) 2026-06-23 01:52:35 -04:00
ohif-bot
1f6670c602 chore(version): Update package versions to 3.13.0-beta.99 [skip ci] 2026-06-20 15:33:27 +00:00
diattamo
b9cde4981a
feat(testing): OHIF Test Agent Skills (#5993) 2026-06-20 11:30:27 -04:00
ohif-bot
fc994fe525 chore(version): Update package versions to 3.13.0-beta.98 [skip ci] 2026-06-20 15:17:28 +00:00
diattamo
ada2da34f0
test(contour): contour interactions delete segment (#6069)
---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2026-06-20 11:14:07 -04:00
diattamo
5955151ba1
test(contour): contour color change coverage (#6042) 2026-06-20 11:11:57 -04:00
ohif-bot
419366bd25 chore(version): Update package versions to 3.13.0-beta.97 [skip ci] 2026-06-19 16:37:27 +00:00
Bill Wallace
b59eb30b0f
fix: Release deploy of docs (#6095)
A netlify fix that needs to be verified on master
2026-06-19 12:34:07 -04:00
ohif-bot
57d03fc722 chore(version): Update package versions to 3.13.0-beta.96 [skip ci] 2026-06-19 15:36:51 +00:00
Bill Wallace
17e12d53e5
fix: pnpm related dependency bugs - nothing funcitonal (#6094)
Testing this needs a release and there are no functional changes, only pnpm migration issues, so merging early for test.

* fix: pnpm related dependency bugs - nothing funcitonal

* Undo peer dependency due to non-pnpm integration issues
2026-06-19 11:33:45 -04:00
ohif-bot
ffd7884ed1 chore(version): Update package versions to 3.13.0-beta.95 [skip ci] 2026-06-18 15:25:00 +00:00
Joe Boccanfuso
0dd6571505
chore(testing): Setup new self-hosted test runner (#6067) 2026-06-18 11:21:41 -04:00
ohif-bot
6e2d53f280 chore(version): Update package versions to 3.13.0-beta.94 [skip ci] 2026-06-17 14:54:34 +00:00
Joe Boccanfuso
6acbbc0271
fix(security): Update dependencies to fix security vulnerabilities. (#6085) 2026-06-17 10:51:03 -04:00
ohif-bot
780d172201 chore(version): Update package versions to 3.13.0-beta.93 [skip ci] 2026-06-16 16:02:34 +00:00
Bill Wallace
256b8347e7
fix(docs): remove stray tool-call tags breaking the MDX build (#6081)
This is a fix to pnpm deployment which needs testing as the final part of origin/master release
No functional changes

* fix(docs): remove stray tool-call tags breaking the MDX build

platform/docs/docs/migration-guide/3p12-to-3p13/build-tooling.md ended with
two orphan closing tags (leftover tool-call serialization artifacts):

  </content>
  </invoke>

Docusaurus compiles Markdown as MDX (JSX-aware), so the orphan closing tag
failed the docs build:

  MDX compilation failed ... Unexpected closing slash in tag, expected an
  open tag first (build-tooling.md line 402)

This was the remaining blocker for build-and-deploy-docs once the
--no-frozen-lockfile change let the install step succeed. A scan of the docs
tree found no other such artifacts. Verified locally: docusaurus build now
generates static files with no MDX errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Update lockfile and avoid freshness check on every command

* fix(release): keep workspace:* in the repo, concretize only at publish

The release flow rewrote internal @ohif/* dependency specifiers to the concrete
version and committed them, so pnpm-lock.yaml (which records workspace links)
drifted from the manifests on every version bump. The resulting
ERR_PNPM_OUTDATED_LOCKFILE broke every frozen install: Netlify (viewer-dev),
the docs deploy, pnpm's pre-run deps check, and post-merge installs.

Keep workspace:* everywhere in the committed repo and move the concrete-version
substitution to publish time only:

- publish-version.mjs: bump each package's own `version` field only; stop
  rewriting @ohif/* dependency/peerDependency specifiers.
- publish-package.mjs: publish with `pnpm publish --no-git-checks` instead of
  `npm publish`. pnpm rewrites workspace:* to the exact version in the published
  tarball; npm would publish the literal "workspace:*", which npm/yarn consumers
  cannot resolve.
- One-time: revert the 25 workspace manifests' @ohif/* specifiers to workspace:*
  (version fields untouched) and regenerate pnpm-lock.yaml to match.

Because internal deps are workspace:* (links, not versions in the lockfile),
version bumps no longer change pnpm-lock.yaml, so it stays in sync and frozen
installs keep working.

Verified: `pnpm install --frozen-lockfile` passes, and `pnpm pack` of @ohif/core
emits a tarball whose @ohif/ui dependency is the exact version (3.13.0-beta.92),
not workspace:*.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(ci): correct build-docs install comment for workspace:* release flow

publish-version.mjs no longer rewrites @ohif/* deps to concrete versions, so
the old comment was stale. Internal deps stay workspace:* and the lockfile
stays consistent; pnpm publish concretizes only the published tarball.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(docs): use --frozen-lockfile now that the lockfile no longer drifts

With internal deps as workspace:* the lockfile stays in sync across version
bumps, so the docs deploy can install frozen -- failing fast on genuine
lockfile drift instead of silently reconciling. The --no-frozen-lockfile
workaround is no longer needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: use --frozen-lockfile in CI install steps now that the lockfile is stable

Internal @ohif/* deps are workspace:* so pnpm-lock.yaml no longer drifts; the
UNIT_TESTS/BUILD/NPM_PUBLISH installs can run frozen and fail fast on genuine
drift. Kept --no-frozen-lockfile only where it is still required: the Dockerfile
(platform/docs is excluded from the build context) and the playwright CS3D-version
step (mutates @cornerstonejs versions before installing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: stability of seg load mpr test

* Better drag fix for crosshairs

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 11:59:04 -04:00
ohif-bot
da9fd9057d chore(version): Update package versions to 3.13.0-beta.92 [skip ci] 2026-06-13 01:01:06 +00:00
Bill Wallace
5cb2fb41f8
fix: pnpm deploy bugs for docs and viewer-dev (#6077)
Merging for testing since this can't be tested without a merge to master

fix(ci): use --no-frozen-lockfile for docs deploy (pnpm migration)

The Build and Deploy Docs workflow ran `pnpm install --frozen-lockfile` and
failed on every master push after the pnpm migration:

  [ERR_PNPM_OUTDATED_LOCKFILE] pnpm-lock.yaml is not up to date with
  platform/core/package.json
    - @ohif/ui (lockfile: workspace:*, manifest: 3.13.0-beta.90)

publish-version.mjs rewrites @ohif/* workspace deps from "workspace:*" to the
concrete release version and commits that bump to master, so the committed
manifests intentionally drift from pnpm-lock.yaml. Every CircleCI job already
passes --no-frozen-lockfile for exactly this reason (pnpm-workspace.yaml sets
frozenLockfile:true as the default); the docs workflow was the lone job still
using frozen and so broke the docs deploy.

Switch the docs install to --no-frozen-lockfile to match the rest of CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@

* fix(ci): write npm auth token to ~/.npmrc so publishes authenticate

NPM_PUBLISH wrote the registry auth token to ~/repo/.npmrc (the repo root),
but publish-package.mjs does process.chdir(packageDirectory) and runs
`npm publish` from inside each package (platform/ui, extensions/*, ...). npm
reads the project .npmrc from that package dir and the user .npmrc from
$HOME -- it never walks up to ~/repo/.npmrc -- so every publish failed with
ENEEDAUTH ("need auth ... requires you to be logged in").

publish-package.mjs catches and swallows per-package publish errors, so
NPM_PUBLISH still exited 0 and reported green; the breakage was silent. As a
result no @ohif/* package newer than 3.13.0-beta.89 (the last publish before
the pnpm migration) reached npm -- beta.90 and beta.91 are missing and the
beta dist-tag is stuck at beta.89.

Write the token to ~/.npmrc (npm per-user config, read regardless of cwd)
instead. This also stops clobbering the committed workspace-config .npmrc
(node-linker=hoisted) at the repo root, which the in-job pnpm install/build
relies on. Applied to all three auth steps for consistency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(docker): copy preinstall.js before pnpm install in builder stage

The builder stage copies only the package.json manifests, runs
`pnpm install --no-frozen-lockfile`, and copies the rest of the source
afterward (for layer caching). But the root package.json defines a
"preinstall" lifecycle script (node preinstall.js) that pnpm runs at the
start of install -- before the source copy -- so the script file was absent
and install aborted:

  . preinstall$ node preinstall.js
  Error: Cannot find module '/usr/src/app/preinstall.js' (MODULE_NOT_FOUND)
  ERROR: process "pnpm install --no-frozen-lockfile" did not complete

This surfaced once the .npmrc COPY fix (#6076) let the build advance past
the earlier COPY failure. preinstall.js is self-contained (it no-ops without
GITHUB_TOKEN and guards the AGENTS.md/CLAUDE.md symlink with existsSync), so
copying just the script into the early manifest layer is sufficient.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(build): declare postcss plugins in platform/app for Docker build

The shared root postcss.config.js (which platform/app re-exports) loads
postcss-import, postcss-preset-env, and cssnano, but none were declared in
platform/app or the root. They only reached node_modules by being hoisted
from platform/docs (postcss-import, postcss-preset-env) and transitively
(cssnano). The Docker image excludes platform/docs via .dockerignore and
libs/@cornerstonejs is not a workspace member, so under pnpm's stricter
node-linker=hoisted the app build failed to resolve the plugins:

  Loading PostCSS "postcss-preset-env" plugin failed:
  Cannot find module 'postcss-preset-env'

(The accompanying "Can't resolve assets/woff2/latin.woff2" error was a
cascade from the broken PostCSS chain and clears with this fix.)

Declare the three plugins the config explicitly loads as devDependencies of
platform/app, pinned to the versions already resolved by working builds
(postcss-import@14.1.0, postcss-preset-env@7.8.3, cssnano@5.1.15), so the
build no longer depends on incidental hoisting from docs.

The lockfile was regenerated against a workspace:* baseline so the only
@ohif change is none -- the diff adds just the postcss plugin trees, keeping
specifiers at workspace:* per the repo's convention. Verified by building the
full Docker image locally: rspack compiles with 0 errors and the image
exports successfully.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 20:57:11 -04:00
ohif-bot
a219822ad1 chore(version): Update package versions to 3.13.0-beta.91 [skip ci] 2026-06-13 00:10:16 +00:00
Bill Wallace
e391e6de5f
fix: Fix the deployment to viewer-dev caused by missing .npmrc (#6076)
@
fix(ci): restore committed .npmrc in Docker build jobs (pnpm migration)

The pnpm migration (#6031) made the Dockerfile COPY .npmrc into the
builder stage because pnpm needs the workspace config it carries
(node-linker=hoisted, link-workspace-packages). But the Docker jobs still
ran `rm -f ./.npmrc` immediately before `docker build` -- a leftover from
when .npmrc only held npm credentials. NPM_PUBLISH overwrites the tracked
.npmrc with a publish auth token and persists it to the workspace, so the
Docker jobs deleted the file the Dockerfile requires:

  ERROR [builder 6/13] COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
  failed to compute cache key: "/.npmrc": not found

DEPLOY_MASTER therefore failed at DOCKER_BETA_PUBLISH on every master
build after the migration (NPM_PUBLISH still pushed the version bump first,
so npm packages shipped but the latest-beta image never did -- leaving
viewer-dev.ohif.org stuck on the prior beta).

Replace `rm -f ./.npmrc` with `git checkout -- .npmrc` in the four Docker
*build* jobs so the committed, token-free workspace-config .npmrc is
restored before the build. The two manifest jobs keep `rm` since they do
not build from the Dockerfile.


@

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 20:05:44 -04:00
ohif-bot
a64fceb009 chore(version): Update package versions to 3.13.0-beta.90 [skip ci] 2026-06-12 20:08:30 +00:00
Bill Wallace
f1cc2ee13a
feat: Update to use pnpm (#6031)
* Initial pnpm change

* Install/update in pnpm sort of works

* Attempt to fix install

* fix pnpm

* Refactor build process to use RSPack instead of Webpack across multiple extensions and modes. Update package.json scripts for development and production builds, and adjust webpack configuration files to accommodate new plugin imports and settings.

* Update RSPack dependencies to version 2.0.0 across the project, enhancing compatibility and performance. Refactor webpack configuration in multiple extensions and modes to utilize the new library structure for UMD output. Adjust package.json scripts and settings for improved build processes.

* Implement migration guide for OHIF 3.13, detailing the transition from Webpack to Rspack v2, the shift to pnpm workspaces, and the increase in minimum Node.js version to 24. Include new build commands, plugin replacements, and updates to package configurations across the monorepo.

* Fix some dev:fast bugs

* Move netlify to top level and remove webpack builds

* Update version script to deal with lerna missing

* chore(tests): Update multiple screenshot test images for various specs

* feat(screenshot-reviewer): Add screenshot review tool and update package.json scripts

* fix(DICOMSRDisplayTool): Improve actor presence check in viewport

* chore(tests): Update multiple screenshot assets for various specs

* chore(tests): Integrate waitForPaintToSettle and waitForViewportsRendered in multiple specs for improved rendering stability

* chore(tests): Update screenshot assets for SEGHydration and SEGNoHydration specs

* test: update progressive loading screenshots

* jest 30 test fixes for compatibility with pnpm cs3d

* Use correct setDisplaySets instead of setDataId

* fix: Naming change for LegacyVolumeViewport3D

* Update to allow tolerance for contour tests

* update

* fix

* refactor: Replace instanceof checks with utility functions for viewport type validation

* fix: Update createSegmentationForViewport to handle undefined displaySetInstanceUID gracefully

* bun lock

* chore(pnpm): align workspace setup to cornerstone3D and address PR review

- .npmrc / pnpm-workspace.yaml: mirror cs3d (node-linker hoisted,
  strict-peer-dependencies=false, link/prefer workspace packages,
  minimumReleaseAge, frozenLockfile); sync axios 1.17.0 + tmp override
- root: packageManager pnpm@11.4.0, engines pnpm >=11, wire preinstall.js
- restore preinstall.js (token/private-repo + CLAUDE->AGENTS symlink)
- playwright.yml: keep base CS3D-integration workflow, switch only the
  package manager (bun/yarn -> pnpm) and node 20 -> 24
- webpack.base.js: revert prod devtool to source-map (drop hidden-source-map)
- remove @percy/cypress; .netlify + cli templates engines pnpm >=11
- Dockerfile pin pnpm@11; document tests/globalSetup.ts warmup
- drop dicom-sr -> measurement-tracking edge (breaks pnpm cyclic dep)

* chore(pnpm): regenerate lockfile and restore cs3d:* dev scripts

- Regenerate pnpm-lock.yaml against the resolved workspace (cycle removed,
  cs3d 4.22.10, workspace:* internal deps).
- Restore cs3d:checkout/check/build/watch/install/link/unlink helper scripts
  (dropped when taking the PR's root scripts), converted yarn -> pnpm since
  the local cornerstone3D checkout is now pnpm too.

* Merge base branch issues

* Link ohif app

* ci: pin pnpm to 11.4.0 in action-setup (was version: latest)

version: latest floats across runs and breaks reproducibility. Pin to the repo's packageManager version (pnpm@11.4.0) in both playwright and build-docs workflows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: Accidental commit of .npmrc config

* Fix build issues

* build fix

* Dependency fixes

* fix: Dependency bug in app on extensions/modes

* Fix output configuration

* Replace percy screenshots with native cypress

* Remove percy screenshot entirely

* fix: Build issues

* force click to prevent canvas cover issues

* Enable swiftshader

* Update launch of electron

* fix: Broken size calculation

* Empty change to force a re-build

* fix(cypress): generate ui-next tailwind classes; drop unsupported electron arg

Tailwind only scanned @ohif/ui-next via ../../node_modules/@ohif/ui-next, which no longer resolves under the pnpm layout, so ui-next-unique classes (e.g. toolbar split-button sizing) were never generated. Those buttons' hit-boxes collapsed, producing Cypress 'covered by element' failures on toolbar interactions. Scan ui-next by direct filesystem path like ui/extensions/modes.

Also stop pushing --enable-unsafe-swiftshader into Electron's launchOptions.args (Electron ignores it and warns); it is supplied via ELECTRON_EXTRA_LAUNCH_ARGS in CI. The arg is still pushed for non-electron chromium (local chrome).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(e2e): disable dev-server overlay during coverage runs

The rspack dev-server client overlay injects an iframe (id=rspack-dev-server-client-overlay) that intercepts pointer events, causing Playwright/Cypress clicks on toolbar buttons to time out (e.g. MicroscopyPanel). Disable the overlay when COVERAGE=true (e2e/Playwright webServer) while keeping it for normal local dev.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(microscopy): provide dicom-microscopy-viewer runtime assets

The microscopy viewer is loaded at runtime via peerImport('dicom-microscopy-viewer'), which fetches the copied asset served at /dicom-microscopy-viewer/. Two issues prevented those assets from reaching dist, so the viewer never initialized and drawing produced no measurement row:

1) dicom-microscopy-viewer was declared in no workspace package.json (only under libs/@cornerstonejs), so it was not installed at root node_modules and the pluginConfig copy source did not exist. Declare it (0.48.6, matching libs) in the microscopy extension.

2) createCopyPluginToDist appended the public/dist folder name to entries that specify an explicit directory, breaking the dicom-microscopy-viewer public entry (looked for .../dynamic-import/public). Use an explicit directory as-is; only append the folder name for package-derived entries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Update to pnpm 11.5.2 and cs3d 5.0.2

* Lock file

* fix: Update to newer versions of actions to try to fix hang

* Remove unnecessary version

* fix: Tests that are flaky

* PR comments

* Incorrect run name

* docs: Explain pluginConfig tooling

* Updated notes on pluginConfig and PR comments

* fix: resolve plugins in rsbuild build and fix directory-based asset copy

rsbuild.config.ts: merge getPluginResolveAliases() and an @ohif/app$ alias into resolve.alias, and add resolve.modules (root, platform/app, platform/ui node_modules) so extensions resolve their shared @ohif/* imports. Brings the rsbuild (dev:fast) path to parity with webpack.pwa.js / webpack.base.js.

writePluginImportsFile.js: treat a 'directory' on an extension/mode entry as the package root and copy its public/ and dist/ subdirs instead of the whole directory; public-section entries keep copying their directory verbatim via a new literalDirectory flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: share resolve alias/modules between webpack and rsbuild configs

Extract the resolve.alias (@ohif/app, @, @components, ...) and the node_modules search paths into .webpack/resolveConfig.js, the single source of truth consumed by both webpack.base.js (and every per-package webpack.prod/.dev.js that merges it) and rsbuild.config.ts. getModules(srcDir) appends the building package's own source root, preserving the previous per-package behavior. Ends the drift where the rsbuild path kept missing aliases/module paths that webpack.base.js already had.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add a timeout on git fetch and avoid all the unnecessary mirrros

* PR comments and tests

* Remove cache to prevent corrupting the cache key/setup.

* Fix stale/corrupted pnpm installer

---------

Co-authored-by: Alireza <ar.sedghi@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 16:04:56 -04:00
ohif-bot
571c2b4ff0 chore(version): Update package versions to 3.13.0-beta.89 [skip ci] 2026-06-10 00:29:36 +00:00
Alireza
6dd150d401
fix: ohif tests to run with cornerstone 3d 5.0 (#6043)
* chore(tests): Update multiple screenshot test images for various specs

* feat(screenshot-reviewer): Add screenshot review tool and update package.json scripts

* fix(DICOMSRDisplayTool): Improve actor presence check in viewport

* chore(tests): Update multiple screenshot assets for various specs

* chore(tests): Integrate waitForPaintToSettle and waitForViewportsRendered in multiple specs for improved rendering stability

* chore(tests): Update screenshot assets for SEGHydration and SEGNoHydration specs

* test: update progressive loading screenshots

* jest 30 test fixes for compatibility with pnpm cs3d

* Use correct setDisplaySets instead of setDataId

* fix: Naming change for LegacyVolumeViewport3D

* Update to allow tolerance for contour tests

* update

* fix

* refactor: Replace instanceof checks with utility functions for viewport type validation

* fix: Update createSegmentationForViewport to handle undefined displaySetInstanceUID gracefully

* bun lock

* fix: Install cs3d with pnpm instead of bun

* Update node version for playwright

* Update to v5.0.0 of cs3d

* fix: Build dependency

* audit

* Change to a web await retry assert

* Fix timing related test failures

* fix: Freehand close

---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2026-06-09 20:25:14 -04:00
ohif-bot
a27f779f0b chore(version): Update package versions to 3.13.0-beta.88 [skip ci] 2026-06-05 12:51:24 +00:00
Joe Boccanfuso
99c6154343
fix(security): Patch axios vulnerabilities. (#6063) 2026-06-05 08:46:09 -04:00
ohif-bot
0885468bb9 chore(version): Update package versions to 3.13.0-beta.87 [skip ci] 2026-05-29 00:13:12 +00:00
Joe Boccanfuso
9396e01107
chore: Fix netlify deploy by updating various @babel dependencies. (#6049) 2026-05-28 20:08:43 -04:00
ohif-bot
1aaae125ea chore(version): Update package versions to 3.13.0-beta.86 [skip ci] 2026-05-28 19:03:08 +00:00
Joe Boccanfuso
ff3e855809
chore: Fix netlify deploy. (#6047) 2026-05-28 14:57:52 -04:00
ohif-bot
550dec742d chore(version): Update package versions to 3.13.0-beta.85 [skip ci] 2026-05-28 16:33:24 +00:00
Joe Boccanfuso
daae4c144e
feat(WorkList): New Study List (WorkList based on ui-next); old study list renamed to LegacyWorklist (#6005)
---------

Co-authored-by: Dan Rukas <dan.rukas@gmail.com>
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2026-05-28 12:26:30 -04:00
ohif-bot
9e7a3586ce chore(version): Update package versions to 3.13.0-beta.84 [skip ci] 2026-05-27 14:43:26 +00:00
Joe Boccanfuso
bdcafc12de
fix(security): Add tmp as a resolution to fix security vulnerability. (#6044) 2026-05-27 10:38:45 -04:00
ohif-bot
5ea22fc34f chore(version): Update package versions to 3.13.0-beta.83 [skip ci] 2026-05-26 16:28:06 +00:00
Ghadeer Albattarni
0c1ca6b04a
fix(segmentation): restore navigation for duplicated contour segments (#6038)
---------

Co-authored-by: diattamo <mmddiatta@gmail.com>
2026-05-26 12:22:46 -04:00
ohif-bot
9a2d2c3d13 chore(version): Update package versions to 3.13.0-beta.82 [skip ci] 2026-05-22 17:53:48 +00:00
Ghadeer Albattarni
5762095cfa
test(contour): add test for RT contour position after viewport maximize/restore (#6036) 2026-05-22 13:49:00 -04:00
ohif-bot
becd208924 chore(version): Update package versions to 3.13.0-beta.81 [skip ci] 2026-05-21 01:24:47 +00:00
diattamo
2258c7957a
feat(tests): Add tests for duplicating contour segments (#6002) 2026-05-20 21:18:58 -04:00
ohif-bot
d186749c5a chore(version): Update package versions to 3.13.0-beta.80 [skip ci] 2026-05-20 01:46:13 +00:00
Bill Wallace
09093b3f0a
Update to use beta 5.0 of CS3D (#5904)
* Update to use beta 5.0

* fix: unit tests

* fix: Allow slow server/test rendering to still work

* Update tests to use viewport grid compare

* Fix storage of stale retries actual/diff files

* cs3d linking fixees

* Linking for cs3d metadata

* Use metadata import for suv scaling

* Fix suv import

* Use type for import

* fix: Metadata registration ordering

* Use beta for upstream

* Merge from origin/master

* Revert tests to master

* Undo beta version changes

* Revert bun.lock file to that of master.

* Reduce test failure retries to one per test. Cap the number of failed total tests to 10. Replace the JSON reporter with the HTML reporter.

* Add generic provider back in

* Deprecate OHIF versions of cs3d utils

* fix

* Undo some cs3d beta dependencies so this PR could be merged

* Build fix

* Fixing build issues

* build issue

* bun lock

* Temporary cache clear to resolve build issue

* Cache bust

* Remove the babel hoist fix attempt

* Cache netlify toml issue

* Fix netlify cache issue

* Try to fix cache dependency issue

* Try update resolutions

* babel hoist fix

* fix: Remove netlify cache bust

* Revert pinned versions

* PR comments on duplicated suv-factors

* Undo lock changes and isEqual add

---------

Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
Co-authored-by: Alireza <ar.sedghi@gmail.com>
2026-05-19 21:41:37 -04:00
ohif-bot
ef8bb5559a chore(version): Update package versions to 3.13.0-beta.79 [skip ci] 2026-05-19 20:04:38 +00:00
Ghadeer Albattarni
95251ab670
fix(seg): prevent viewport orientation change when loading SEG in manual grid layout (#6021) 2026-05-19 15:59:42 -04:00
ohif-bot
1e91285c60 chore(version): Update package versions to 3.13.0-beta.78 [skip ci] 2026-05-19 19:33:03 +00:00
Ghadeer Albattarni
27af6821b5
fix(measurements): read displayName in SplineROI and PlanarFreehandROI reports (#5908) 2026-05-19 15:28:05 -04:00
ohif-bot
6f92234111 chore(version): Update package versions to 3.13.0-beta.77 [skip ci] 2026-05-19 18:50:02 +00:00
Ghadeer Albattarni
527cfb0ae8
fix(segmentation): remove thresholdRange label in double-range tool settings (#5931) 2026-05-19 14:45:25 -04:00
ohif-bot
64fa240dfa chore(version): Update package versions to 3.13.0-beta.76 [skip ci] 2026-05-19 13:29:35 +00:00
Ghadeer Albattarni
77cd9394d2
test(segmentation): add e2e test for deleting segmentation on second reload (#6013) 2026-05-19 09:24:49 -04:00
ohif-bot
8e8f574174 chore(version): Update package versions to 3.13.0-beta.75 [skip ci] 2026-05-18 14:40:35 +00:00
Joe Boccanfuso
fe16e80cf3
test(e2e): update Playwright screenshots and add better tests and assertions for area calculations (#6022)
* Add (better) assertions for area calculation fixes.
* Add SVG and measurement side panel area for freehand ROI test.
* Add Playwright viewport screenshot scope migration guide.
* Update cornerstonjs dependencies to 4.22.8. Ensure all versions of @babel/preset-env are 7.29.5.

---------

Co-authored-by: Ghadeer Albattarni <165973963+GhadeerAlbattarni@users.noreply.github.com>
2026-05-18 10:36:05 -04:00
ohif-bot
cb0cbe702c chore(version): Update package versions to 3.13.0-beta.74 [skip ci] 2026-05-15 19:11:45 +00:00
Joe Boccanfuso
55b46f39c6
fix(security): Update various dependencies to fix security vulnerabilities. (#6023) 2026-05-15 15:07:13 -04:00
ohif-bot
01b5c86826 chore(version): Update package versions to 3.13.0-beta.73 [skip ci] 2026-05-13 17:16:29 +00:00
Ibrahim
f231eff598
feat(llm): add instructions md file for claude and other LLM tools (#6017)
* add claude instructions file

* update structure

* Update llm/AGENTS.md

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update llm/AGENTS.md

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* move to root

* symlink

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-05-13 13:11:55 -04:00
ohif-bot
b058ec7c72 chore(version): Update package versions to 3.13.0-beta.72 [skip ci] 2026-05-12 17:26:24 +00:00
Ghadeer Albattarni
397aa4d0e3
fix(measurement-tracking): restore tracked state on undo after Delete all (#5994)
* fix: always show delete confirmation when measurements exist
2026-05-12 13:22:25 -04:00
ohif-bot
8cd8ccc163 chore(version): Update package versions to 3.13.0-beta.71 [skip ci] 2026-05-07 22:51:22 +00:00
Ghadeer Albattarni
37e19f734a
fix: remove waitForVolumeLoad from main toolbar page object (#6004) 2026-05-07 18:47:27 -04:00
ohif-bot
13305de99c chore(version): Update package versions to 3.13.0-beta.70 [skip ci] 2026-05-07 19:46:54 +00:00
Ghadeer Albattarni
9cf3149d2a
test(e2e): replace waitForVolumeLoad with viewport render waits (#6000) 2026-05-07 15:42:12 -04:00
ohif-bot
fbea158473 chore(version): Update package versions to 3.13.0-beta.69 [skip ci] 2026-05-06 17:53:50 +00:00
Joe Boccanfuso
5e624f1387
fix(security): Patch axios security vulnerabilities. (#5998)
Patch axios security vulnerabilities.
2026-05-06 13:49:13 -04:00
ohif-bot
d792bf7f80 chore(version): Update package versions to 3.13.0-beta.68 [skip ci] 2026-05-05 19:27:31 +00:00
Joe Boccanfuso
979d619f7c
fix(ToolGroupService): Centralize tool binding persistence and provide API to add and remove persisted bindings. (#5989)
* Reset crosshair modifier to default when resetting user preferences.

* Apply bindings when loading persisted bindings.
2026-05-05 15:22:43 -04:00
ohif-bot
5bccf0a753 chore(version): Update package versions to 3.13.0-beta.67 [skip ci] 2026-04-30 14:18:14 +00:00
Dan Rukas
691e26731f
feat(ui-next): Adds toggle state for ToolButton and Crosshair example (#5914)
Co-authored-by: sedghi <ar.sedghi@gmail.com>
2026-04-30 10:13:58 -04:00
ohif-bot
e6816f6de4 chore(version): Update package versions to 3.13.0-beta.66 [skip ci] 2026-04-29 16:01:57 +00:00
Joe Boccanfuso
468e5734a9
fix(config url): Hardening fetch options. (#5985) 2026-04-29 11:57:33 -04:00
ohif-bot
90abd00936 chore(version): Update package versions to 3.13.0-beta.65 [skip ci] 2026-04-29 14:37:25 +00:00
Ghadeer Albattarni
62620afb9b
test( zoomIn-tool): verify horizontal flip is preserved in magnified view (#5983) 2026-04-29 10:32:27 -04:00
ohif-bot
03860030bd chore(version): Update package versions to 3.13.0-beta.64 [skip ci] 2026-04-27 17:00:15 +00:00
Ghadeer Albattarni
f8ccf9ff2e
fix(seg): prevent segmentations from spreading to all viewports before hydration confirmation in 3D four-up (#5967) 2026-04-27 12:55:12 -04:00
ohif-bot
c9c4f11b82 chore(version): Update package versions to 3.13.0-beta.63 [skip ci] 2026-04-27 11:11:23 +00:00
Joe Boccanfuso
f3cca21e49
fix(security): refine dynamic config URL trust policy and document trusted-origin behavior (#5973)
* fix(security): refine dynamic config URL trust policy and document trusted-origin behavior

* Update documentation for Authorization and Authentication.

* Add isSameOrigin to resolveConfigFetchPolicy return value.
2026-04-27 07:06:06 -04:00
ohif-bot
5ba8339037 chore(version): Update package versions to 3.13.0-beta.62 [skip ci] 2026-04-23 17:49:30 +00:00
Joe Boccanfuso
1fc97fea43
fix(security): Patch protobufjs for CVE-2026-41242. (#5974) 2026-04-23 13:45:09 -04:00
ohif-bot
7008029cdc chore(version): Update package versions to 3.13.0-beta.61 [skip ci] 2026-04-23 12:51:09 +00:00
Joe Boccanfuso
8fc0dc16e9
feat(slice scrollbar): Integrate ViewportSliceProgressScrollbar with customizations and docs (#5960)
Co-authored-by: Dan Rukas <dan.rukas@gmail.com>
2026-04-23 08:46:03 -04:00
ohif-bot
8a2fee4d8a chore(version): Update package versions to 3.13.0-beta.60 [skip ci] 2026-04-21 16:00:22 +00:00
Joe Boccanfuso
eede569a88
fix(configuration): Harden dynamic datasource URL trust boundaries and credential handling. (#5963)
* fix(configuration): Harden dynamic datasource URL trust boundaries and credential handling.

* Remove testing configuration.

* Update policies for runtime ?url=... datasource loading.

* PR feedback.
2026-04-21 11:55:14 -04:00
ohif-bot
51e6b35dbb chore(version): Update package versions to 3.13.0-beta.59 [skip ci] 2026-04-16 21:51:43 +00:00
Ghadeer Albattarni
792f78e35f
chore: update Cornerstone3D to v4.21.7 (#5964) 2026-04-16 17:47:00 -04:00
Bill Wallace
cc5dc20164
fix: Use newer ONNX version and load without errors (#5854)
* fix: Use newer ONNX version and load without errors

* Only changes to enable SAM again

* fix(seg hydration): auto-hydrate RT struct on second load with disableConfirmationPrompts (#5875)

* chore(version): Update package versions to 3.13.0-beta.34 [skip ci]

* fix(Threshold tool): Threshold tool no longer becomes deselected when the Dynamic option is selected (#5884)

fix(Threshold tool): Added 'ThresholdCircularBrushDynamic' to the toolNames array so the evaluator correctly recognizes it as an active state for the Threshold button when Dynamic mode is selected.

* chore(version): Update package versions to 3.13.0-beta.35 [skip ci]

* fix: Modalities in study list should select starts with as primary (#5886)

* chore(version): Update package versions to 3.13.0-beta.36 [skip ci]

* fix(security): Bump tar version to address CVE-2026-31802. (#5893)

* chore(version): Update package versions to 3.13.0-beta.37 [skip ci]

* fix(segmentation): Display "No description S:{series number} {modality}" for segmentations with no label. (#5874)

* Bump CS3D dependency to get the fallbackLabel field additions.

* chore(version): Update package versions to 3.13.0-beta.38 [skip ci]

* fix(window level): The window level value is not displayed by default on all the viewports when selecting common/custom layout and TMTV. (#5865)

* fix(window level): Set up listener for viewport availability such that the initial window level can be read and displayed.

* PR feedback.

* PR feedback.

---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>

* chore(version): Update package versions to 3.13.0-beta.39 [skip ci]

* fix(security): Bump flattened version to address CVE-2026-32141. (#5897)

* chore(version): Update package versions to 3.13.0-beta.40 [skip ci]

* fix(sr-hydration): enable hydration and arrow navigation for 3D SR measurements (#5887)

Joe is away, so approving based on the code having the requested change, and otherwise looking good/passing tests.

* fix(sr-hydration): enable hydration and arrows navigation for 3D SR measurements

* test: add automated test for SR measurement navigation with arrows after hydration

* add cross-study warning in the 3D branch

* test: address reviewer feedback for the test

* fix: support 3D and 2D annotations for SR hydration

* test: improve navigation to first image

---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>

* chore(version): Update package versions to 3.13.0-beta.41 [skip ci]

* feat: Add combined build (#5895)

* Add combined build

* Link script location update

* Security and validation fixes

* Allow specifying target path in PR description

* fix: Version match

* Fix build detection issue

* fix: Playwright deploy

* Separate out the branch merge guard

* Update docs and link info

* test: Update the layout change to wait for network idle

* Move audit late so the rest of the build can be worked on

* Add text with network check to ensure we see this change is updated

* Attempt to fix the mpr loading on ohif-downstream

* PR review comments

* Update docs

* Update to CS3D 4.20.0

* PR comments

* Add log on ohif-integration builds

* Update build test

* Removed unused space to kickoff build

* chore(version): Update package versions to 3.13.0-beta.42 [skip ci]

* fix(SR): Added support for spline and live wire SR items. (#5870)

* fix(SR): Added support for spline and live wire SR items.

* Apply suggestion from @greptile-apps[bot]

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Add a script to checkout a worktree for test builds

* fix: Allow download for testing sr validator

* Remove script that wasn't intended to be included

* Bump CS3D version.

* PR comments - simplify code and use single codepath for download

* Allow both download and save buttons for SEG and RTSTRUCT

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>

* chore(version): Update package versions to 3.13.0-beta.43 [skip ci]

* chore(tests): contour segment interactions e2e tests - rename and togglevisibility (#5891)

* chore(version): Update package versions to 3.13.0-beta.44 [skip ci]

* chore(refactor): use public appConfig getter instead of private _appConfig field (#5923)

* chore(version): Update package versions to 3.13.0-beta.45 [skip ci]

* refactor(tests): update viewport page object usage to async and update all effected tests (#5927)

* chore(version): Update package versions to 3.13.0-beta.46 [skip ci]

* fix: prevent black viewport when navigating series with client-created segmentation (#5919)

* chore(version): Update package versions to 3.13.0-beta.47 [skip ci]

* fix(measurement): Restore viewport interactivity when deleting in-progress Spline or Livewire measurement (#5905)

* chore(version): Update package versions to 3.13.0-beta.48 [skip ci]

* fix(segmentation): restrict overlay segmentation menu to same frame of reference as viewport background display set  (#5900)

- Add FrameOfReferenceUID to SEG and RTSTRUCT displaySet in SOP Class Handlers so the FOR is available for filtering
- Sync optimisticOverlayDisplaySets when background display set changes so the overlay menu reflects the correct state after a background switch
- Add FOR matching guard to the hydrate segmentation synchronizer to prevent the hydration synchronizer from blindly mirroring segmentations from a source viewport to a target viewport if their primary Frames of Reference do not align.
- fix segmentation overlay order reversal on viewport re-render

* chore(version): Update package versions to 3.13.0-beta.49 [skip ci]

* fix(security): update dependencies to fix security vulnerabilities (#5936)

* chore(version): Update package versions to 3.13.0-beta.50 [skip ci]

* fix(security): Update yarn.lock that was missed in PR #5936. (#5940)

* chore(version): Update package versions to 3.13.0-beta.51 [skip ci]

* feat(component): Adds SmartScrollbar to ui-next - OHIF-2558 (#5924)

Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>

* fix(defaultRouteInit): pass sorted display sets to hanging protocol for deterministic viewport order (#5933)

fix: pass sorted display sets to hanging protocol for deterministic viewport order

The `applyHangingProtocol` function already sorts display sets by modality
priority and series number into `sortedDisplaySets`, but the unsorted
`displaySets` array was being passed to `hangingProtocolService.run()`.

This caused non-deterministic viewport ordering across page loads because
`displaySetService.getActiveDisplaySets()` returns display sets in creation
order, which depends on asynchronous network responses.

Made-with: Cursor

* chore(version): Update package versions to 3.13.0-beta.52 [skip ci]

* revert: rename DisplaySet.frameOfReferenceUID back to FrameOfReferenceUID (#5943)

* chore(version): Update package versions to 3.13.0-beta.53 [skip ci]

* fix(cornerstone): read FrameOfReferenceUID from display set in viewport service (#5950)

* chore(version): Update package versions to 3.13.0-beta.54 [skip ci]

* fix: ignore auth in git (#5955)

* chore(version): Update package versions to 3.13.0-beta.55 [skip ci]

* ONNX latest version

* chore(version): Update package versions to 3.13.0-beta.56 [skip ci]

* bun lock

* fix high sev mathjs issue

* Revert onnx changes

* Update to recent CS3D version

* Undo unneeded change

* Add null check

* Undo unneeded change

---------

Co-authored-by: Ghadeer Albattarni <165973963+GhadeerAlbattarni@users.noreply.github.com>
Co-authored-by: ohif-bot <danny.ri.brown+ohif-bot@gmail.com>
Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: diattamo <mmddiatta@gmail.com>
Co-authored-by: Pedro Köhler <pedrokohlerbh@gmail.com>
Co-authored-by: Dan Rukas <dan.rukas@gmail.com>
Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
Co-authored-by: Alireza <ar.sedghi@gmail.com>
2026-04-15 12:04:00 -04:00
ohif-bot
31727c0668 chore(version): Update package versions to 3.13.0-beta.58 [skip ci] 2026-04-14 18:00:56 +00:00
Bill Wallace
760adc50d6
Reformat in HP MPR (#5717)
* Reformat in HP MPR

* fix: Orientation markers should work
2026-04-14 13:56:20 -04:00
ohif-bot
6e4ef68a83 chore(version): Update package versions to 3.13.0-beta.57 [skip ci] 2026-04-13 13:36:14 +00:00
Alireza
f5a66b9eea
fix: Enhance multiframe instance handling and improve instance valida… (#5956)
fix: Enhance multiframe instance handling and improve instance validation

- Updated `getDisplaySetInfo` to ensure local file frame imageIds resolve correctly, falling back to the source multiframe instance when necessary.
- Improved instance filtering in `isDisplaySetReconstructable` to handle undefined instances more robustly.
- Modified `App` prop types to make `routerBasename` optional and added new props for loading indicators and data sources.
- Refactored `CinePlayer` component to replace Button with a div for better accessibility and interaction.
2026-04-13 09:31:28 -04:00
ohif-bot
3069d33c99 chore(version): Update package versions to 3.13.0-beta.56 [skip ci] 2026-04-10 20:57:30 +00:00
Bill Wallace
f6bbd5c779
fix: A couple of changes to enable cs3d integration build (#5944)
* fix: A couple of changes to enable cs3d integration build

* Bun update

* fix: Crosshairs tests due to order changes

* Fix a race in DicomTagBrowser.spec.ts and update the comparison for the screenshot for seg hydration.

* Fix sorting issues by using consistent sort

* fix: Inconsistency in scoord loader.  Will need an update to screenshot

* Fix crosshairs stability issues and random order issues in Scoord

* Update the comparison image

* Update navigate image

* fix: Axios issue

* Update to current CS3D
2026-04-10 16:51:37 -04:00
ohif-bot
66c8e0c195 chore(version): Update package versions to 3.13.0-beta.55 [skip ci] 2026-04-10 19:21:27 +00:00
Alireza
961eea1c39
fix: ignore auth in git (#5955) 2026-04-10 15:16:55 -04:00
ohif-bot
0ddf32eef2 chore(version): Update package versions to 3.13.0-beta.54 [skip ci] 2026-04-08 15:59:34 +00:00
Ghadeer Albattarni
d219171e25
fix(cornerstone): read FrameOfReferenceUID from display set in viewport service (#5950) 2026-04-08 11:54:05 -04:00
ohif-bot
6cbad4fab7 chore(version): Update package versions to 3.13.0-beta.53 [skip ci] 2026-04-06 16:08:24 +00:00
Ghadeer Albattarni
0e933c256e
revert: rename DisplaySet.frameOfReferenceUID back to FrameOfReferenceUID (#5943) 2026-04-06 12:03:28 -04:00
ohif-bot
74e656b174 chore(version): Update package versions to 3.13.0-beta.52 [skip ci] 2026-04-06 13:27:41 +00:00
Pedro Köhler
68b10d365a
fix(defaultRouteInit): pass sorted display sets to hanging protocol for deterministic viewport order (#5933)
fix: pass sorted display sets to hanging protocol for deterministic viewport order

The `applyHangingProtocol` function already sorts display sets by modality
priority and series number into `sortedDisplaySets`, but the unsorted
`displaySets` array was being passed to `hangingProtocolService.run()`.

This caused non-deterministic viewport ordering across page loads because
`displaySetService.getActiveDisplaySets()` returns display sets in creation
order, which depends on asynchronous network responses.

Made-with: Cursor
2026-04-06 09:23:37 -04:00
Dan Rukas
91a8715795
feat(component): Adds SmartScrollbar to ui-next - OHIF-2558 (#5924)
Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
2026-04-06 09:21:13 -04:00
ohif-bot
fdf26aa8b9 chore(version): Update package versions to 3.13.0-beta.51 [skip ci] 2026-04-03 18:22:25 +00:00
Joe Boccanfuso
84ddf78c87
fix(security): Update yarn.lock that was missed in PR #5936. (#5940) 2026-04-03 14:17:40 -04:00
ohif-bot
d7ab507f5b chore(version): Update package versions to 3.13.0-beta.50 [skip ci] 2026-04-02 22:10:46 +00:00
Joe Boccanfuso
5358a3985a
fix(security): update dependencies to fix security vulnerabilities (#5936) 2026-04-02 18:06:19 -04:00
ohif-bot
d670fb168c chore(version): Update package versions to 3.13.0-beta.49 [skip ci] 2026-04-02 12:57:23 +00:00
Ghadeer Albattarni
b9029ef6f8
fix(segmentation): restrict overlay segmentation menu to same frame of reference as viewport background display set (#5900)
- Add FrameOfReferenceUID to SEG and RTSTRUCT displaySet in SOP Class Handlers so the FOR is available for filtering
- Sync optimisticOverlayDisplaySets when background display set changes so the overlay menu reflects the correct state after a background switch
- Add FOR matching guard to the hydrate segmentation synchronizer to prevent the hydration synchronizer from blindly mirroring segmentations from a source viewport to a target viewport if their primary Frames of Reference do not align.
- fix segmentation overlay order reversal on viewport re-render
2026-04-02 08:52:23 -04:00
ohif-bot
a875d7de1f chore(version): Update package versions to 3.13.0-beta.48 [skip ci] 2026-04-01 20:57:11 +00:00
Ghadeer Albattarni
7929f0898f
fix(measurement): Restore viewport interactivity when deleting in-progress Spline or Livewire measurement (#5905) 2026-04-01 16:52:14 -04:00
ohif-bot
f999f3eab5 chore(version): Update package versions to 3.13.0-beta.47 [skip ci] 2026-03-28 23:24:57 +00:00
Ghadeer Albattarni
e66fb5a2b8
fix: prevent black viewport when navigating series with client-created segmentation (#5919) 2026-03-28 19:20:49 -04:00
ohif-bot
efac34dfe5 chore(version): Update package versions to 3.13.0-beta.46 [skip ci] 2026-03-27 20:41:39 +00:00
Ghadeer Albattarni
2a728b244c
refactor(tests): update viewport page object usage to async and update all effected tests (#5927) 2026-03-27 16:37:42 -04:00
ohif-bot
485f0d75d9 chore(version): Update package versions to 3.13.0-beta.45 [skip ci] 2026-03-27 17:18:56 +00:00
Pedro Köhler
d7dd12ad67
chore(refactor): use public appConfig getter instead of private _appConfig field (#5923) 2026-03-27 13:14:13 -04:00
ohif-bot
9718ecc407 chore(version): Update package versions to 3.13.0-beta.44 [skip ci] 2026-03-27 13:59:09 +00:00
diattamo
fe762b68e8
chore(tests): contour segment interactions e2e tests - rename and togglevisibility (#5891) 2026-03-27 09:54:36 -04:00
ohif-bot
87eeefdf1b chore(version): Update package versions to 3.13.0-beta.43 [skip ci] 2026-03-17 13:03:35 +00:00
Joe Boccanfuso
1d4802c2a3
fix(SR): Added support for spline and live wire SR items. (#5870)
* fix(SR): Added support for spline and live wire SR items.

* Apply suggestion from @greptile-apps[bot]

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Add a script to checkout a worktree for test builds

* fix: Allow download for testing sr validator

* Remove script that wasn't intended to be included

* Bump CS3D version.

* PR comments - simplify code and use single codepath for download

* Allow both download and save buttons for SEG and RTSTRUCT

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2026-03-17 08:59:47 -04:00
ohif-bot
69d8ad63f3 chore(version): Update package versions to 3.13.0-beta.42 [skip ci] 2026-03-17 11:55:55 +00:00
Bill Wallace
1df671e9ab
feat: Add combined build (#5895)
* Add combined build

* Link script location update

* Security and validation fixes

* Allow specifying target path in PR description

* fix: Version match

* Fix build detection issue

* fix: Playwright deploy

* Separate out the branch merge guard

* Update docs and link info

* test: Update the layout change to wait for network idle

* Move audit late so the rest of the build can be worked on

* Add text with network check to ensure we see this change is updated

* Attempt to fix the mpr loading on ohif-downstream

* PR review comments

* Update docs

* Update to CS3D 4.20.0

* PR comments

* Add log on ohif-integration builds

* Update build test

* Removed unused space to kickoff build
2026-03-17 07:52:17 -04:00
ohif-bot
15ef4c2e0f chore(version): Update package versions to 3.13.0-beta.41 [skip ci] 2026-03-16 13:40:10 +00:00
Ghadeer Albattarni
7a38903b19
fix(sr-hydration): enable hydration and arrow navigation for 3D SR measurements (#5887)
Joe is away, so approving based on the code having the requested change, and otherwise looking good/passing tests.

* fix(sr-hydration): enable hydration and arrows navigation for 3D SR measurements

* test: add automated test for SR measurement navigation with arrows after hydration

* add cross-study warning in the 3D branch

* test: address reviewer feedback for the test

* fix: support 3D and 2D annotations for SR hydration

* test: improve navigation to first image

---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2026-03-16 09:36:56 -04:00
ohif-bot
40a472a9b1 chore(version): Update package versions to 3.13.0-beta.40 [skip ci] 2026-03-13 19:28:45 +00:00
Joe Boccanfuso
d8d376edaf
fix(security): Bump flattened version to address CVE-2026-32141. (#5897) 2026-03-13 15:25:21 -04:00
ohif-bot
d0bab783c7 chore(version): Update package versions to 3.13.0-beta.39 [skip ci] 2026-03-13 14:15:45 +00:00
Joe Boccanfuso
fe1ecfe0cc
fix(window level): The window level value is not displayed by default on all the viewports when selecting common/custom layout and TMTV. (#5865)
* fix(window level): Set up listener for viewport availability such that the initial window level can be read and displayed.

* PR feedback.

* PR feedback.

---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2026-03-13 10:12:07 -04:00
ohif-bot
54d9ea39bf chore(version): Update package versions to 3.13.0-beta.38 [skip ci] 2026-03-12 22:42:59 +00:00
Joe Boccanfuso
7b5d0ce40e
fix(segmentation): Display "No description S:{series number} {modality}" for segmentations with no label. (#5874)
* Bump CS3D dependency to get the fallbackLabel field additions.
2026-03-12 18:39:14 -04:00
ohif-bot
15f14e34e4 chore(version): Update package versions to 3.13.0-beta.37 [skip ci] 2026-03-12 00:56:47 +00:00
Joe Boccanfuso
d015b2e32a
fix(security): Bump tar version to address CVE-2026-31802. (#5893) 2026-03-11 20:53:17 -04:00
ohif-bot
197b80eefd chore(version): Update package versions to 3.13.0-beta.36 [skip ci] 2026-03-11 18:31:47 +00:00
Bill Wallace
b83e978df8
fix: Modalities in study list should select starts with as primary (#5886) 2026-03-11 14:28:20 -04:00
ohif-bot
4c2bd8d5ed chore(version): Update package versions to 3.13.0-beta.35 [skip ci] 2026-03-09 19:47:36 +00:00
Joe Boccanfuso
889026f8a9
fix(Threshold tool): Threshold tool no longer becomes deselected when the Dynamic option is selected (#5884)
fix(Threshold tool): Added 'ThresholdCircularBrushDynamic' to the toolNames array so the evaluator correctly recognizes it as an active state for the Threshold button when Dynamic mode is selected.
2026-03-09 15:43:56 -04:00
ohif-bot
1aaf569825 chore(version): Update package versions to 3.13.0-beta.34 [skip ci] 2026-03-09 13:11:54 +00:00
Ghadeer Albattarni
6f773f9972
fix(seg hydration): auto-hydrate RT struct on second load with disableConfirmationPrompts (#5875) 2026-03-09 09:08:21 -04:00
ohif-bot
0f822aaf48 chore(version): Update package versions to 3.13.0-beta.33 [skip ci] 2026-03-05 20:12:21 +00:00
Joe Boccanfuso
61a1fccd7d
fix(security): Bump svgo and tar to fix vulnerabilities. (#5877) 2026-03-05 15:09:05 -05:00
ohif-bot
c842642341 chore(version): Update package versions to 3.13.0-beta.32 [skip ci] 2026-03-05 18:00:06 +00:00
TFRadicalImaging
70f76aeba1
feat(ecg): add DICOM ECG waveform extension (#5856)
* feat(ecg): add DICOM ECG waveform extension

Introduce @ohif/extension-dicom-ecg for rendering DICOM waveform (ECG)
data. Register the extension in the basic mode and pluginConfig.json,
and remove ECG from NON_IMAGE_MODALITIES so waveform display sets are
handled by the new viewport.

* refactor(ecg): move ECG support into cornerstone extension per review feedback

- Remove standalone dicom-ecg extension; fold all ECG functionality into
  the cornerstone extension as requested by reviewer
- Add ECG SOP class handler (DicomEcgSopClassHandler) to the cornerstone
  extension getSopClassHandlerModule, registering ECG waveform metadata
  via genericMetadataProvider on display set creation
- Move ECG helpers (buildEcgModule, decodeInt16Multiplex, base64ToArrayBuffer)
  into extensions/cornerstone/src/utils/ecgMetadata.ts
- Handle ECGViewport in CornerstoneViewportService._setDisplaySets by
  detecting ECGViewport instanceof and calling setEcg(imageId) directly,
  so OHIFCornerstoneViewport can be used without a custom ECG viewport component
- Add ECG support to getCornerstoneViewportType utility
- Update basic mode to reference the cornerstone extension's ECG SOP handler
  and use the base cornerstone viewport for ECG display sets
- Migrate ecgMetadata and getCornerstoneViewportType tests

* chore: revert bun.lock to upstream origin/master

---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2026-03-05 12:56:52 -05:00
ohif-bot
30bb5d0652 chore(version): Update package versions to 3.13.0-beta.31 [skip ci] 2026-03-04 20:32:11 +00:00
Ghadeer Albattarni
2358a73c3c
fix(microscopy): rename measurement in microscopy mode (#5866) 2026-03-04 15:28:41 -05:00
ohif-bot
df22109013 chore(version): Update package versions to 3.13.0-beta.30 [skip ci] 2026-03-04 20:25:47 +00:00
Ghadeer Albattarni
7e10830d58
fix(segmentation): segment bidirectional tool error and show toast notification when no segment is drawn (#5861) 2026-03-04 15:22:10 -05:00
ohif-bot
6db0bc2d4c chore(version): Update package versions to 3.13.0-beta.29 [skip ci] 2026-03-04 17:27:01 +00:00
Joe Boccanfuso
be8e266a80
fix(security): Address various security vulnerabilities. (#5869) 2026-03-04 12:23:49 -05:00
ohif-bot
b3e5f64694 chore(version): Update package versions to 3.13.0-beta.28 [skip ci] 2026-03-03 21:56:46 +00:00
diattamo
f2ed4c9e6f
test(ContourSegNavigation): Add e2e tests for contour segmentation navigation (#5834)
---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-03 16:52:03 -05:00
ohif-bot
b6d362f375 chore(version): Update package versions to 3.13.0-beta.27 [skip ci] 2026-03-02 13:16:59 +00:00
Ghadeer Albattarni
92011c587f
chore(test): add FreehandROI test that MEASUREMENT_ADDED does not fire when clicking annotation text (#5846) 2026-03-02 08:12:28 -05:00
ohif-bot
c9dea4e6cc chore(version): Update package versions to 3.13.0-beta.26 [skip ci] 2026-02-28 09:54:42 +00:00
Silas Shellenbarger
6a3caa1ac9
fix capture not maintaining flip/rotate state (#5843)
* fix capture not maintaining flip/rotate state

* fix capture not maintaining flip/rotate state

---------

Co-authored-by: silasshellenbarger <sdshe@Silas-Computer.localdomain>
2026-02-28 04:50:35 -05:00
ohif-bot
2b486f16ed chore(version): Update package versions to 3.13.0-beta.25 [skip ci] 2026-02-26 14:54:56 +00:00
Bill Wallace
5370b9382e
fix: Update CS3D 4.18.2 (#5837)
* Update CS3D 4.17.4

* fix: Update to cs3d 4.18.2

* audit issues

* audit fixes

* fix: redos issue

* Simplify dependencies to fix audit alerts.

---------

Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
2026-02-26 09:49:44 -05:00
ohif-bot
67f9de4ab6 chore(version): Update package versions to 3.13.0-beta.24 [skip ci] 2026-02-24 13:57:26 +00:00
Ghadeer Albattarni
d11d6d401b
fix: prevent viewer crash when opening DICOM Tag Browser from empty viewport (#5827) 2026-02-24 08:53:05 -05:00
Ghadeer Albattarni
a8f709982f
fix(ui-next): Add validation for opacity and border inputs in the segmentation panel - OHIF-2332 (#5819) 2026-02-24 08:50:07 -05:00
ohif-bot
9e4c0bb7a2 chore(version): Update package versions to 3.13.0-beta.23 [skip ci] 2026-02-23 22:29:33 +00:00
Bill Wallace
eeca1f7a7b
fix: palette color 8 encoded 16 (#5823)
* fix: Detect 16 bit when 8 declared

* Update bun lock with dcmjs changes

* PR comments

* Update package versions

* fix: Palette color lookup table data for dcmjs parsed data

* fix: Published version should match (#5813)

* fix: Published version should match

* Move version number update to package.json

* fix: Netlify version number update

* Try updating platform app vesion too

* PR comments

---------

Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>

* Update latest cs3d

* formatting

* fix: Use 64k instead of 0 for number of entries

* fix: Revert CS3D version to see test issues

* Updated screenshots for ArrowAnnotate.spec.ts.

* Update CS3D version

* fix: Update cs3d 4.17.4

* Update bun lock

* Update bun

* Downgrade to CS3D 4.17.2 to try to release

---------

Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
2026-02-23 17:24:33 -05:00
ohif-bot
279ae0dfe0 chore(version): Update package versions to 3.13.0-beta.22 [skip ci] 2026-02-23 21:42:30 +00:00
Joe Boccanfuso
928d592333
chore(security): Updates for running bun audit during CI and dependabot PR version update settings (#5835)
* chore(security): Update circleci config to only run bun audit for lockfile changes.

* Switched to eslint 9.39.3 for compatibility with .eslintrc.json.

* Disabled all dependabot pull requests for bun and npm version updates.
2026-02-23 16:37:56 -05:00
ohif-bot
3330d2b52b chore(version): Update package versions to 3.13.0-beta.21 [skip ci] 2026-02-23 14:19:07 +00:00
arul-trenser
b5573caaa6
fix(RTStruct): RTSTRUCT contours rendered on first slice for multi-frame images without IPP (#5811)
Fix RTSTRUCT contours rendered on first slice for multi-frame images without IPP
2026-02-23 09:14:34 -05:00
ohif-bot
cb4397fe31 chore(version): Update package versions to 3.13.0-beta.20 [skip ci] 2026-02-21 23:19:22 +00:00
Bill Wallace
d359a3b784
fix: Published version should match (#5813)
* fix: Published version should match

* Move version number update to package.json

* fix: Netlify version number update

* Try updating platform app vesion too

* PR comments

---------

Co-authored-by: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com>
2026-02-21 18:15:10 -05:00
ohif-bot
ce9f159c21 chore(version): Update package versions to 3.13.0-beta.19 [skip ci] 2026-02-20 22:29:35 +00:00
Joe Boccanfuso
27229609cd
fix(security): CVE-2026-27212 patched. Various dependency updates as a result of CVE-2026-26996. (#5830)
fix(security):  CVE-2026-27212 patched.
Various dependency updates as a result of CVE-2026-26996.
Ultimately CVE-2026-26996 was ignored because it is only exposed in itk-wasm via CLI and OHIF's other use of minimatch is limited to build/dev environments.
2026-02-20 17:25:06 -05:00
ohif-bot
83e687fb02 chore(version): Update package versions to 3.13.0-beta.18 [skip ci] 2026-02-19 01:36:02 +00:00
dxlin
b35354120a
fix(segmentation overlay): update viewport ds list upon seg delete - OHIF-2425 (#5729)
- add segmentationExists check to getSopClassHandlerModule
- now firing SEGMENTATION_REMOVED and SEGMENTATION_REPRESENTATION_REMOVED events
- centralized segmentation removal in a listener
- when a segmentation is deleted (completely), remove it from all viewports it overlays
- when a segmentation is removed from a viewport, remove it as overlay from the viewport

---------

Co-authored-by: Joe Boccanfuso <joe.boccanfuso@radicalimaging.com>
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2026-02-18 20:31:43 -05:00
ohif-bot
c75cbc2a89 chore(version): Update package versions to 3.13.0-beta.17 [skip ci] 2026-02-19 00:08:20 +00:00
Bill Wallace
53c67f3612
fix: Add copy to clipboard option for capture (#5720)
* Add copy to clipboard option for capture

* PR comments

* Added toast feedback and updated labels

---------

Co-authored-by: Dan Rukas <dan.rukas@gmail.com>
2026-02-18 19:03:17 -05:00
ohif-bot
c85ca0e177 chore(version): Update package versions to 3.13.0-beta.16 [skip ci] 2026-02-18 21:51:03 +00:00
Joe Boccanfuso
3d59c0d9d3
fix(security): Bump tar to 7.5.9 and lerna to 9.0.4 to fix CVE-2026-26960. (#5824)
* fix(security): Bump tar to 7.5.9 and lerna to 9.0.4 to fix CVE-2026-26960.
Bump sharp to 0.34.5 to fix tar-fs vulnerabilities.

* Update node version to 20.19.0 in circleci config. Needed for lerna and cypress tests.

* Now installing bun for cypress tests in circleci config.

* Use node version 20.19.0 in netlify config.
2026-02-18 16:46:02 -05:00
1017 changed files with 99844 additions and 59792 deletions

View File

@ -0,0 +1,346 @@
---
name: ohif-test-agent
description: Generate runnable Playwright E2E tests for the OHIF Viewer using its custom fixture system, page objects, and normalized WebGL viewport coordinates. Use this skill whenever the user asks to write, add, modify, or debug tests in an OHIF/Viewers context — including vague asks like "write a test for X" when working in the OHIF repo, tests touching platform/app/tests/, or anything involving Cornerstone viewports, DICOM studies, measurements, segmentations, or OHIF modes/extensions. Prefer this skill over generic test-writing even if the user doesn't say "Playwright" or "E2E" explicitly.
---
# OHIF Test Agent
This skill teaches you to generate correct, runnable Playwright end-to-end tests for the OHIF Viewer. Follow the workflow below.
## Environment model
This package follows the agentskills.io SKILL.md convention. `SKILL.md` is the entire behavior contract — there is no separate runtime entrypoint.
## Workflow: how to write a new OHIF test
1. **Classify the feature.** What area does the test belong to — a measurement tool, segmentation hydration, contour panel interaction, MPR layout, crosshairs, tag browser, etc.? The area determines the mode, the StudyInstanceUID, and the seed spec you'll read.
2. **Read the seed spec.** Consult [references/patterns-by-feature.md](references/patterns-by-feature.md) to find the canonical existing spec for that area. Read it end-to-end before writing. This is the single most important step — OHIF specs follow consistent idioms that are easier to mimic than to reconstruct from first principles. (This mirrors Playwright's own agent guidance: use seed tests as the example for generated tests.)
3. **Scaffold from the template.** Start from [assets/spec-template.ts](assets/spec-template.ts) — or copy the seed spec and adapt.
4. **Look up specifics in the source, not from memory.** The reference files [page-objects.md](references/page-objects.md) and [utilities.md](references/utilities.md) capture the **stable rules** — fixture keys, import conventions, access idioms, the reasons certain things trip people up. They deliberately do not enumerate methods. For the current method surface or a utility's exact signature, open the relevant file under `tests/pages/` or `tests/utils/` — the source evolves, and the source is always right. The seed spec you picked in step 2 is usually the fastest second source, because it co-evolves with the API.
5. **Run the test when execution is available.** `pnpm run test:e2e:ci` runs the whole suite, but for iteration use `TEST_ENV=true pnpm exec playwright test tests/YourNew.spec.ts` (or the Playwright VS Code extension). Invoke Playwright directly for targeted flags; `pnpm run test:e2e -- ...` inserts a `--` separator that can prevent Playwright from parsing options such as `--update-snapshots` and `--reporter`.
6. **If runtime execution is unavailable, do static validation.** Validate import source, fixture keys, normalized viewport usage, UID/mode pairing, and hydration/tracking prompt handling. Then report clearly that execution was not performed.
7. **If it fails, triage before debugging.** Use [references/failure-triage.md](references/failure-triage.md) — most OHIF test failures are timing / hydration, not real regressions.
## Architecture
OHIF uses Playwright with a custom fixture system. Tests are **not** vanilla Playwright — they import `test`, `expect`, and utilities from `./utils`, which re-exports an extended test runner that injects page objects.
```text
playwright.config.ts → Chromium-only, port 3335, data-cy as testId
└─ tests/utils/fixture.ts → Extends playwright-test-coverage, injects page objects
└─ tests/*.spec.ts → Each imports { test, expect, ... } from './utils'
├─ tests/pages/ → Page objects (ViewportPageObject, MainToolbarPageObject, …)
└─ tests/utils/ → Utilities (visitStudy, checkForScreenshot, screenShotPaths, …)
```
Why the custom fixture matters: the page objects are created for each test and bound to the right Playwright `page`. If you `new ViewportPageObject(page)` manually, you skip the fixture wiring and some sub-objects won't resolve correctly.
### Import rule
```ts
// Correct
import { test, expect, visitStudy, checkForScreenshot, screenShotPaths } from './utils';
// Wrong — will compile but fixtures won't be injected
import { test, expect } from '@playwright/test';
```
A few utilities (`press`, `downloadAsString`, the `assert*` helpers) are NOT re-exported from `./utils`. See [references/utilities.md](references/utilities.md) for the correct import path per utility.
## The viewport is WebGL
OHIF renders medical images onto a WebGL canvas. You cannot query *canvas* pixels by CSS selector. Use **normalized coordinates** (01 range, top-left is `{x:0, y:0}`) for clicks and drags, and **visual regression** (screenshot comparison) for canvas assertions. (Not everything in the viewport is canvas — some overlays render as SVG you *can* query via DOM, e.g. a vector overlay's color through `getSvgAttribute`. The canvas rule is about raster output painted onto the WebGL surface.)
```ts
const activeViewport = await viewportPageObject.active;
await activeViewport.normalizedClickAt([{ x: 0.5, y: 0.5 }]); // click center
await activeViewport.normalizedClickAt([{ x: 0.3, y: 0.3 }, { x: 0.7, y: 0.7 }]); // draw two points
await activeViewport.normalizedClickAt([{ x: 0.5, y: 0.5 }], 'right'); // right-click
await activeViewport.normalizedDragAt({
start: { x: 0.3, y: 0.3 },
end: { x: 0.7, y: 0.7 },
});
```
Pixel coordinates (`clickAt`, `doubleClickAt`) exist but prefer normalized for portability across viewport sizes.
For DOM-rendered state (panel counts, dialog text, overlay text values, button enabled states), assert directly:
```ts
await expect(activeViewport.overlayText.bottomRight.instanceNumber).toContainText('17/');
const count = await rightPanelPageObject.measurementsPanel.panel.getMeasurementCount();
expect(count).toBe(1);
```
## Study loading lifecycle
Every test follows this sequence. Skipping steps causes flakiness:
```ts
test.beforeEach(async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000); // 2s delay is the community norm
});
```
`visitStudy` navigates to `/{mode}/ohif?StudyInstanceUIDs={uid}`, waits for `domcontentloaded`, then `networkidle`, then the explicit delay. Default delay is `0`, but most specs pass `2000` to let the first render settle.
Delay by scene type (observed across the current suite, not a rule to apply blindly):
| Scene | Delay |
|-------|-------|
| Default (viewer mode, 2D/MPR/3D layouts, crosshairs) | `2000` |
| `mode: 'tmtv'` | `10000` — PET fusion + SUV calculation takes noticeably longer |
Start at the convention for your scene; ramp only if the test flakes on initial render. 3D layouts in `viewer` mode already stay at `2000` — the stabilization problem there is solved with `attemptAction(() => reduce3DViewportSize(page), 10, 100)`, not with a longer `visitStudy` delay.
If the study has DICOM SEG, RT, or SR data, OHIF asks whether to hydrate. Handle it:
```ts
await leftPanelPageObject.loadSeriesByModality('SEG'); // or 'RTSTRUCT', 'SR'
await page.waitForTimeout(3000); // allow the prompt to appear
await expect(DOMOverlayPageObject.viewport.segmentationHydration.locator).toBeVisible();
await DOMOverlayPageObject.viewport.segmentationHydration.yes.click();
```
The first measurement you create triggers a "start tracking?" prompt:
```ts
await DOMOverlayPageObject.viewport.measurementTracking.confirm.click();
```
For 3D / MPR scenes, wrap stabilization in `attemptAction(() => reduce3DViewportSize(page), 10, 100)` or insert a `page.waitForTimeout(...)` after the layout change before asserting.
## Wait for renders, don't sleep
`page.waitForTimeout(...)` after an action that re-renders the viewport is a smell. The viewports tell us when they're done — use that signal. `tests/utils/waitForViewportsRendered.ts` exposes three helpers, all barrel-exported from `./utils`:
- `waitForViewportRenderCycle(page)` — wait for the next full cycle: a viewport enters `needsRender`, then **all** viewports report `rendered` (and volumes are loaded, by default).
- `waitForViewportsRendered(page)` — only the second half: wait until all viewports are `rendered`. Use this when the action has already requested a render before you started waiting (e.g. a layout change or `loadSeriesByDescription`).
- `waitForAnyViewportNeedsRender(page)` — only the first half. Rarely needed directly.
The canonical idiom — **start the watcher before the action, await it after**:
```ts
// start watching for the next render cycle
const viewportRenderCycle = waitForViewportRenderCycle(page);
await action(); // e.g. segmentationHydration.yes.click(), layoutSelection.MPR.click(), addSegmentation, etc.
// wait for the render to finish
await viewportRenderCycle;
await check(); // e.g. checkForScreenshot, count assertion, overlay text
```
Why "start before"? `waitForViewportRenderCycle` first waits for a viewport to enter `needsRender`. If you start it **after** the action, that transition may already be over and you'll hang until the timeout. Starting it first captures the cycle the action is about to trigger.
When to use which:
| Situation | Helper |
|-----------|--------|
| Click that triggers a re-render and you want to assert after | `waitForViewportRenderCycle(page)` started before the click |
| Layout switch / series load — render already in flight | `await waitForViewportsRendered(page)` after the call |
| Compose with another await (e.g. screenshot the same time as load) | Save the promise, `await` it later |
Replace patterns like this:
```ts
// ❌ Sleep-and-pray
await action();
await page.waitForTimeout(5000);
await checkForScreenshot(...);
// ✅ Wait on the actual signal
const cycle = waitForViewportRenderCycle(page);
await action();
await cycle;
await checkForScreenshot(...);
```
This shaves real wall-clock time off the suite and removes a class of flake (sleep too short → flake; sleep too long → slow). `tests/SEGHydrationFromMPR.spec.ts` is the canonical seed for this pattern.
Caveats:
- These helpers wait on Cornerstone viewport state. They won't help for purely DOM-side state (panel rows appearing, dialogs opening) — for those, prefer `expect(locator).toHaveCount(n)` / `toBeVisible()` which auto-retry, or `expect.toPass({ timeout })`.
- For some actions (hanging-protocol changes are the documented example) the viewport doesn't transition through `needsRender` synchronously — those still need a short `waitForTimeout`. The source comment in `waitForViewportsRendered.ts` calls this out.
## Fixture-injected page objects
Destructure these from the test function argument. **Never `new` them manually.**
```ts
test('my test', async ({
page,
viewportPageObject,
mainToolbarPageObject,
leftPanelPageObject,
rightPanelPageObject,
DOMOverlayPageObject, // note the capital D — this matches the fixture key
notFoundStudyPageObject,
}) => { ... });
```
Two page objects are **not** fixture-injected:
- `DataOverlayPageObject` — reach via `viewportPageObject.getById(id).overlayMenu.dataOverlay`.
- `DicomTagBrowserPageObject` — reach via `DOMOverlayPageObject.dialog.dicomTagBrowser`.
See [references/page-objects.md](references/page-objects.md) for fixture rules and a map of which file covers which concern; read the `.ts` file under `tests/pages/` for the current method surface.
## When the control you need has no page object yet
A spec must not reach for `page.getByTestId(...)` / `getByRole(...)` directly for
application controls. If the button, menu, dialog, or field you need isn't already
exposed by a page object, **add it to one — or create a new page object — instead of
inlining a raw selector.** Raw selectors in a spec are the clearest sign a test was
written without reading the existing suite: they duplicate locators, bypass the
fixture wiring, and rot silently when the DOM changes.
Where new coverage goes:
| What you need | Where it belongs |
|---|---|
| A toolbar button or tool (Zoom, Pan) | a getter on `MainToolbarPageObject`, next to `crosshairs` / `measurementTools` |
| A menu, prompt, context menu, or small dialog | `DOMOverlayPageObject` |
| A substantial dialog with its own fields (User Preferences) | its **own** page object class, reached through `DOMOverlayPageObject` — follow `DicomTagBrowserPageObject` (`DOMOverlayPageObject.dialog.dicomTagBrowser`) |
| Each field/row inside that dialog | a method or sub-object on the dialog's page object — not a raw selector in the spec |
| A side-panel control | `LeftPanelPageObject` / `RightPanelPageObject` |
If the control has no `data-cy`, **add `data-cy` to the source component** and target
it — don't fall back to `getByRole`/text selectors, which are brittle and
locale-sensitive (`testIdAttribute` is `data-cy`, so `getByTestId('Zoom')` resolves
`[data-cy="Zoom"]`). Call out any `data-cy` you add so it ships in the same PR.
**Worked example — "set the Zoom hotkey in User Preferences":** the options menu, the
preferences dialog, each preference field, and the Zoom toolbar button should all be
page-object surface — e.g. `mainToolbarPageObject.zoom`,
`DOMOverlayPageObject.optionsMenu.settings.click()`,
`DOMOverlayPageObject.dialog.userPreferences.hotkey('Zoom').set('q')`. The spec then
reads as intent, not as a pile of `getByTestId` calls.
## Assert the effect, not just the attribute
Prefer asserting the actual rendered result over a proxy attribute. Activating Zoom and
checking `data-active="true"` confirms the *button* toggled — not that zoom works. Drag
on the viewport and assert the image actually zoomed (a viewport-scoped screenshot, or a
measurable state change). Attribute checks are fine as a secondary signal, not the whole
test.
## Visual regression
**Direction:** the suite is moving off *full-app* screenshots — not off screenshots
altogether. "Avoid screenshots" means: don't screenshot the whole page, and don't
screenshot something that has a faithful DOM/state signal. It does **not** mean avoid
screenshots for output that is genuinely canvas-only — for that output a screenshot is
the correct and required tool, and you should use it without apology. Older specs that
screenshot the whole page are the legacy pattern being phased out; viewport-scoped
screenshots are not.
### Screenshot vs. DOM assertion — how to choose
Reach for the cheapest *faithful* signal, in this order:
1. **A faithful DOM/SVG/state signal exists → assert on it.** Panel counts, dialog and
overlay text, enabled/disabled state, and any overlay that renders as SVG (a vector
overlay's color is readable via `getSvgAttribute`) all have a DOM representation — assert
on it directly, no screenshot.
2. **The thing under test is painted onto the WebGL canvas with no DOM representation → a
screenshot is correct and required.** Raster output on the canvas exposes no attribute to
read for a painted pixel. Scope a `checkForScreenshot` to the viewport (pane or grid) and
assert it — this is the right tool, not a last resort, whenever what you're verifying is
the rendered canvas itself.
3. **Never substitute a service/state read for a render assertion.** Reading a service's
state (any `window.services...`) asserts the *data model*, not the pixels the user sees —
it passes even when rendering is broken. `page.evaluate(() => window.services...)` is an
escape hatch for *setup*, not for *appearance* assertions.
For anything drawn onto the WebGL canvas with no DOM signal, compare a screenshot scoped to a specific viewport or the viewport grid:
```ts
await checkForScreenshot({
page,
locator: viewportPageObject.grid, // scope to the viewport grid — not the whole page
screenshotPath: screenShotPaths.length.lengthDisplayedCorrectly,
});
```
`checkForScreenshot` retries up to 10 times at 500 ms intervals. Use `screenShotPaths.<category>.<name>` rather than a hand-typed string — the tree of valid keys lives in `tests/utils/screenShotPaths.ts`.
Rules (apply to all new screenshot assertions):
- **Use the object form.** The positional form is legacy; don't introduce it in new code, and don't treat existing positional-form usage as a pattern to copy.
- **Never screenshot the full app.** Full-page screenshots include panels, toolbars, and dialogs that drift independently of what's under test and make baselines fragile. Scope by passing a `locator``viewportPageObject.grid` for the grid, or a specific viewport pane. A bare `normalizedClip: { x: 0, y: 0, width: 1, height: 1 }` with no `locator` is **not** scoping — it clips to the full page. Use `normalizedClip` only to target a sub-region *of a locator* (e.g. a scrollbar strip). If you reach for `fullPage: true`, stop and pick a locator.
- **Do not tune `maxDiffPixelRatio` or `threshold`** to make a screenshot pass. If a baseline mismatches, regenerate it after a human review of the diff, or fix the underlying flake.
## Playwright config facts worth remembering
| Setting | Value | Why |
|---------|-------|-----|
| `baseURL` | `http://localhost:3335` | OHIF e2e uses 3335, not 3000 |
| `testIdAttribute` | `data-cy` | `getByTestId(...)` maps to `[data-cy="..."]` |
| `browser` | Chromium only | Firefox/WebKit disabled (SharedArrayBuffer + stability) |
| `retries` | 3 in CI, 0 locally | Flaky rendering needs CI retries |
| `workers` | 6 in CI, undefined locally | Parallel execution |
| `globalTimeout` / `timeout` | 800_000 ms | Medical image loads are slow |
| `actionTimeout` | 10_000 ms | Per-action cap |
| `webServer.command` | `cross-env APP_CONFIG=config/e2e.js COVERAGE=true OHIF_PORT=3335 nyc yarn start` | e2e config + coverage |
## Test data: which study for which test
| StudyInstanceUID | Mode | Used for |
|------------------|------|----------|
| `1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5` | `viewer` | General measurements, annotations, context menu |
| `1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785` | `viewer` | 3D, MPR, crosshairs |
| `1.3.6.1.4.1.14519.5.2.1.256467663913010332776401703474716742458` | `viewer` or `segmentation` | Labelmap SEG |
| `1.2.840.113619.2.290.3.3767434740.226.1600859119.501` | `viewer` / `segmentation` / `tmtv` | RTSTRUCT/contour and TMTV |
| `1.3.6.1.4.1.14519.5.2.1.7695.4007.324475281161490036195179843543` | `viewer` | SR hydration |
Full mapping in [references/patterns-by-feature.md](references/patterns-by-feature.md). **Do not invent UIDs** — they must exist on the e2e data server.
## Rules (short, so they're actually read)
1. Import `test`, `expect`, and utilities from `./utils`, not `@playwright/test`.
2. Destructure fixture-injected page objects; don't `new` them.
3. Use normalized coordinates (01) for viewport interactions.
4. Use `visitStudy` with a real UID, correct mode, and a non-zero delay (2000 is conventional).
5. Handle hydration and measurement-tracking prompts where applicable.
6. Choose the faithful signal: DOM/SVG assertions where the rendered result has one (panels, dialogs, overlay text, SVG/vector overlays), a viewport-scoped screenshot when what you're verifying is canvas-only raster output, and never a `window.services` state read standing in for a render check. Screenshots use the object form, scoped via a `locator` — never the full app.
7. Use `data-cy` selectors (already wired via `testIdAttribute`).
8. When an assertion needs retry tolerance, wrap it in `expect.toPass({ timeout })`.
9. Test in the correct mode — segmentation tools aren't available in `viewer` mode.
10. If a utility isn't exported from `./utils`, import from the deeper path (see [references/utilities.md](references/utilities.md)).
11. After an action that re-renders the viewport, prefer `waitForViewportRenderCycle(page)` (started before the action) over `page.waitForTimeout(...)`. See the "Wait for renders, don't sleep" section.
12. Don't inline raw `page.getByTestId(...)` / `getByRole(...)` for app controls. If a control has no page object, create or augment one (see "When the control you need has no page object yet"), adding a `data-cy` to the source if needed.
13. Assert the actual effect (e.g. the image zoomed), not just a proxy attribute like `data-active`.
## Pre-output self-check (mandatory)
Before returning a generated OHIF test, confirm all items:
1. Imports `test`/`expect` from `./utils` (not `@playwright/test`).
2. Uses fixture-injected keys and exact casing (especially `DOMOverlayPageObject`).
3. Uses normalized viewport interactions (`normalizedClickAt` / `normalizedDragAt`) unless there is a strong reason otherwise.
4. Uses a valid canonical StudyInstanceUID and compatible mode.
5. Handles hydration or measurement tracking prompts when the workflow requires them.
6. Uses the faithful signal for each assertion — DOM/SVG where the result has a DOM representation, a viewport-scoped screenshot when what's verified is canvas-only raster output, and never a `window.services` state read in place of a render check. Any `checkForScreenshot` call uses the object form, scoped via a `locator` (viewport pane or grid) — no full-app screenshots.
7. Replaces `page.waitForTimeout(...)` after viewport-rendering actions with `waitForViewportRenderCycle(page)` (started before the action) — keeps `waitForTimeout` only for non-render waits like the hydration prompt in `beforeEach`.
8. If execution was skipped, states that explicitly and provides concrete run commands.
9. Every application control is reached through a page object — no raw `getByTestId`/`getByRole` in the spec for buttons, menus, dialogs, or fields. Any control not already covered was added to the right page object (or a new one), with a source `data-cy` if it lacked one.
10. Assertions verify the real effect where feasible (e.g. the image visibly zoomed), not only an attribute toggle.
## Output contract (for non-executing agents)
When execution cannot be performed in the current environment, the response should include:
1. The test code.
2. Assumptions made (if any).
3. Static checks that were verified.
4. What still must be run locally and exact commands to run.
## When to consult each reference
- **Before writing** → [references/patterns-by-feature.md](references/patterns-by-feature.md). Pick the seed spec for the feature area and read it. The seed spec is the closest thing to a live API example because it co-evolves with the code.
- **For a stable rule or idiom** (fixture keys, import paths, panel-access order, capital-D quirk, object-param convention) → [references/page-objects.md](references/page-objects.md), [references/utilities.md](references/utilities.md).
- **For a method name, property, or signature** → read the source under `tests/pages/` or `tests/utils/`. Do not rely on a static table for these; they drift as the code is refactored.
- **When a test fails** → [references/failure-triage.md](references/failure-triage.md).

View File

@ -0,0 +1,54 @@
import {
checkForScreenshot,
expect,
screenShotPaths,
test,
visitStudy,
waitForViewportRenderCycle,
} from './utils';
test.beforeEach(async ({ page }) => {
// Pick the right UID + mode for your feature (see references/patterns-by-feature.md)
const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000);
});
test.describe('FEATURE NAME', () => {
test('describes the behaviour in one sentence', async ({
page,
viewportPageObject,
mainToolbarPageObject,
rightPanelPageObject,
DOMOverlayPageObject, // capital D on purpose
}) => {
// 1. Arrange — select a tool, open a panel, etc.
// 2. Act — interact with the viewport.
// Prefer normalized (01) coordinates:
// const activeViewport = await viewportPageObject.active;
// await activeViewport.normalizedClickAt([{ x: 0.3, y: 0.3 }, { x: 0.7, y: 0.7 }]);
// For actions that re-render the viewport, gate on the render cycle
// instead of sleeping — start the watcher BEFORE the action:
// const cycle = waitForViewportRenderCycle(page);
// await action();
// await cycle;
// 3. Handle prompts (first measurement prompts for tracking; SEG/RT/SR prompts for hydration)
// await DOMOverlayPageObject.viewport.measurementTracking.confirm.click();
// 4. Assert canvas output via visual regression — object form, scoped to the
// viewport via a locator (not the full page). normalizedClip is only for
// clipping to a sub-region of that locator.
// await checkForScreenshot({
// page,
// locator: viewportPageObject.grid,
// screenshotPath: screenShotPaths.YOUR_CATEGORY.YOUR_KEY,
// });
// 5. Assert DOM state directly
// const count = await rightPanelPageObject.measurementsPanel.panel.getMeasurementCount();
// expect(count).toBe(1);
});
});

View File

@ -0,0 +1,72 @@
# Failure triage
Before debugging, classify. Most OHIF test failures are timing or hydration — not real regressions.
| Category | Symptom | Fix |
|----------|---------|-----|
| Timing | Element not visible, action timeout | Add / increase the `delay` param of `visitStudy`; for actions that re-render viewports, use `waitForViewportRenderCycle(page)` (started before the action) instead of `waitForTimeout`; wrap the assertion in `expect.toPass({ timeout })` |
| Selector | Element not found | Verify `data-cy` on the target; confirm the panel is open (`toggle()` / `select()` before interacting); check for capital `D` in `DOMOverlayPageObject` when destructuring |
| Hydration | Segmentation/RT/SR not interactive | Ensure the `segmentationHydration.yes.click()` fired; wait for an observable hydrated state such as measurement/segment rows or the target series overlay, then wait for the resulting viewport render |
| Data | Study not found, empty viewport | Confirm the UID is in the canonical list (see [patterns-by-feature.md](patterns-by-feature.md)); confirm the mode supports the feature (segmentation tools aren't in `viewer` mode) |
| Visual drift | Screenshot mismatch but feature works | Have a human review the diff, then regenerate the baseline with `TEST_ENV=true pnpm exec playwright test --update-snapshots`. Do not adjust `maxDiffPixelRatio` or `threshold` to make a failing screenshot pass. |
| Real regression | Feature is actually broken | Report as a bug — this is the test doing its job |
## Prefer render-cycle waits over sleeps
If you're tempted to add `await page.waitForTimeout(2000)` after an action, ask whether the action re-rendered the viewport. If it did, use:
```ts
const cycle = waitForViewportRenderCycle(page);
await action();
await cycle;
await check();
```
The watcher must be created **before** the action — it waits for `needsRender` first, and that transition is gone by the time the action returns. See the "Wait for renders, don't sleep" section in [SKILL.md](../SKILL.md) and `tests/SEGHydrationFromMPR.spec.ts`.
### When the cycle helper times out at `waitForAnyViewportNeedsRender`
Symptom: the test fails inside `waitForAnyViewportNeedsRender` after 5s, with the action having actually completed in the UI. The action just doesn't transition the viewport through `needsRender` synchronously. Known cases:
- Hanging-protocol changes.
- **RTSTRUCT / contour segmentation hydration confirm.** SEG (labelmap) hydration does fire `needsRender`; contour does not. The fix is to gate on the actual end-state — for hydration in `beforeEach`, `await expect(page.getByTestId('data-row')).toHaveCount(N)` is the right wait.
Don't react by raising the cycle's timeout — the transition isn't coming. Replace the cycle wrapper with an auto-retrying DOM/SVG assertion, or `expect.toPass({ timeout })` around the assertion block.
An immediate `waitForViewportsRendered(page)` can also return too early when a click dispatches work through an asynchronous state machine: the old viewport is already `rendered` before the new series or annotations are applied. In that case, first wait for the target state (for example, hydrated measurement rows or the expected series overlay), then call `waitForViewportsRendered(page)` to settle that state's render.
## The `toPass` pattern
When an assertion needs to wait for async render / propagation:
```ts
await expect(async () => {
await rightPanelPageObject.labelMapSegmentationPanel.panel.segmentByText('Spleen').click();
await expect(activeViewport.overlayText.bottomRight.instanceNumber).toContainText('17/');
}).toPass({ timeout: 10_000 });
```
`toPass` reruns the assertion block until it succeeds or the timeout expires — cleaner than a hand-rolled retry loop and surfaces the last failure reason if it times out.
## Common `DOMOverlayPageObject` mistake
The fixture key is capital-D `DOMOverlayPageObject`, not lowercase. If your destructure is silently `undefined`, check the casing.
## `press` import mistake
`press` is NOT re-exported from `./utils`. `import { press } from './utils'` resolves to `undefined` and fails at runtime. Use:
```ts
import { press } from './utils/keyboardUtils';
await press({ page, key: 'ArrowDown', nTimes: 50 }); // object param, not (page, key)
```
## Visual regression iteration
Screenshots live under `tests/screenshots/chromium/<testFilePath>/`. To accept new output as the baseline:
```sh
TEST_ENV=true pnpm exec playwright test tests/YourSpec.spec.ts --update-snapshots
```
Review the resulting PNGs carefully — an agent-accepted baseline that's subtly wrong is worse than a failing test.

View File

@ -0,0 +1,120 @@
# OHIF Page Object guide
> This file documents the **stable structural rules** of the page object system. For the current list of methods and properties on any class, **read the source under `tests/pages/`** — it is always authoritative, and it evolves as the product does. A static method table in a reference file goes stale the moment someone refactors; the source does not.
## How to discover the API of a page object
1. Find the relevant class in `tests/pages/`. File names match class names.
2. Read it end-to-end once — most are under a few hundred lines.
3. Some classes compose sub-objects (e.g. `RightPanelPageObject` holds a measurementsPanel, contourSegmentationPanel, labelMapSegmentationPanel, tmtvPanel, etc.). Those sub-objects usually live in the same file or a sibling under `tests/pages/`.
4. To see how a method is actually used, grep `tests/` or open the seed spec listed in [patterns-by-feature.md](patterns-by-feature.md). Real usage beats a synthesized signature every time.
Do not try to memorize a method surface from this file — it intentionally does not list one. It lists only the rules you cannot derive from the source by reading a single file.
---
## Stable rules
### Fixture keys (case-sensitive)
These are injected via `tests/utils/fixture.ts`. Destructure them from the test function's first argument — do not `new` them, because the fixture wires sub-objects to the correct `page` and hand-constructed instances skip that wiring.
- `viewportPageObject`
- `mainToolbarPageObject`
- `leftPanelPageObject`
- `rightPanelPageObject`
- `DOMOverlayPageObject`**capital D**. A silent `undefined` destructure is almost always a casing typo here.
- `notFoundStudyPageObject`
If the fixture file is updated and new keys are added, they will show up there first — check it if something feels missing.
### Non-fixture page objects
Some page object classes are not fixture-injected. They are reached through an injected fixture:
- `DicomTagBrowserPageObject` → via `DOMOverlayPageObject.dialog.dicomTagBrowser`
- `DataOverlayPageObject` → via `viewportPageObject.getById(viewportId).overlayMenu.dataOverlay`
Both can be constructed manually (`new DataOverlayPageObject(page)`) if a test really needs a fresh instance, but the accessor path is the idiomatic one.
### Viewport wrapper vs. viewport instance
`viewportPageObject` is a **wrapper**. You almost always want a specific viewport out of it first:
- `await viewportPageObject.active` — the currently focused viewport
- `viewportPageObject.getAll()` — every viewport in the grid
- `viewportPageObject.getNth(i)` — zero-indexed
- `viewportPageObject.getById(cornerstoneViewportId)` — e.g. `'default'`, `'ctAXIAL'`
The object these return is the one with `normalizedClickAt`, `normalizedDragAt`, `overlayText`, `nthAnnotation`, etc. Reach for the viewport instance first, then call methods on it.
### Panel access order
Every `rightPanelPageObject` sub-panel follows the same three-step idiom: **open the side panel, `.select()` the sub-panel tab, then interact with `.panel.*`**. Skipping either of the first two is the most common cause of "element not found".
```ts
await rightPanelPageObject.toggle();
await rightPanelPageObject.measurementsPanel.select();
const count = await rightPanelPageObject.measurementsPanel.panel.getMeasurementCount();
```
The exact row/action methods vary by panel — check the source file for the one you need.
### Layout identifiers are camelCase JS properties
`mainToolbarPageObject.layoutSelection.<layout>.click()` — access layouts by camelCase property name (e.g. `threeDFourUp`, `axialPrimary`), not with bracket-escaped DICOM-ish strings like `['3DFourUp']`. This is a convention enforced by how the class exposes its tools.
### Sub-tools auto-open their dropdown
Tools nested inside a toolbar dropdown (measurement tools, more tools, layouts) each expose a `.click()` that opens the parent menu for you. You almost never need to open the menu first. `await mainToolbarPageObject.measurementTools.length.click()` does both the expand and the select.
### When the control you need isn't covered yet — create or augment
The page objects here cover what the suite currently exercises. When your test needs a
control that isn't exposed, **extend the page object system rather than dropping a raw
`page.getByTestId(...)` into the spec.** A raw selector in a spec is the clearest tell
that the author didn't read the existing tests — it duplicates a locator that should
live in one place and bypasses the fixture wiring.
| What you need | Where it goes | Precedent to copy |
|---|---|---|
| A toolbar button/tool (Zoom, Pan) | a getter on `MainToolbarPageObject` | `crosshairs`, `measurementTools` |
| A menu / prompt / context menu / small dialog | `DOMOverlayPageObject` | `viewport.measurementTracking`, `dialog.input` |
| A large dialog with its own fields (User Preferences) | a **new** page object class reached via `DOMOverlayPageObject` | `DicomTagBrowserPageObject` (`DOMOverlayPageObject.dialog.dicomTagBrowser`) |
| Individual fields/rows in that dialog | methods/sub-objects on the dialog's page object | `DicomTagBrowserPageObject.seriesSelect` |
| Side-panel controls | `LeftPanelPageObject` / `RightPanelPageObject` | existing sub-panels |
Two more expectations:
- **Missing `data-cy`?** Add it to the source component and target it; don't fall back
to `getByRole`/text. `testIdAttribute` is `data-cy`, so `getByTestId('Zoom')` resolves
`[data-cy="Zoom"]`. Mention any attribute you added so it ships in the same PR.
- **Mirror the existing shape.** A new getter should look like its neighbors (return a
locator, or a small object with `.click()` etc.) so the new surface is
indistinguishable from what was already there.
Example — a "set the Zoom hotkey" test should make `mainToolbarPageObject.zoom`, an
options-menu accessor on `DOMOverlayPageObject`, and a `userPreferences` dialog page
object (with a per-hotkey field accessor) exist — then the spec reads as steps, not
selectors.
---
## Page object map — what each class is *for*
This table exists to help you pick the right file to open, not to enumerate methods. Source of truth for any specific method remains the `.ts` file.
| Class | File | Covers |
|-------|------|--------|
| ViewportPageObject | `tests/pages/ViewportPageObject.ts` | Cornerstone viewports — clicks, drags, overlays, annotations, crosshairs |
| MainToolbarPageObject | `tests/pages/MainToolbarPageObject.ts` | Top toolbar — measurement tools, more tools, layouts, crosshairs, pan |
| LeftPanelPageObject | `tests/pages/LeftPanelPageObject.ts` | Study browser — thumbnails, load by modality or description |
| RightPanelPageObject | `tests/pages/RightPanelPageObject.ts` | Side panels — measurements, contour seg, labelmap seg, TMTV, microscopy |
| DOMOverlayPageObject | `tests/pages/DOMOverlayPageObject.ts` | DOM overlays — dialogs, hydration/tracking prompts, context menus, tag-browser accessor |
| NotFoundStudyPageObject | `tests/pages/NotFoundStudyPageObject.ts` | Study-not-found error page |
| DicomTagBrowserPageObject | `tests/pages/DicomTagBrowserPageObject.ts` | Tag-browser dialog (non-fixture; reach via `DOMOverlayPageObject.dialog`) |
| DataOverlayPageObject | `tests/pages/DataOverlayPageObject.ts` | Data-overlay menu (non-fixture; reach via `viewport.overlayMenu`) |
If the directory adds or renames a file, that diff is your first clue and this table is your second — trust the directory.
For live usage, the seed spec in [patterns-by-feature.md](patterns-by-feature.md) shows how a class is actually called; the source file tells you everything else that's on it.

View File

@ -0,0 +1,148 @@
# Seed specs by feature area
> When writing a new OHIF test, find the closest feature area below and read the listed spec in full before writing. Playwright's own guidance says the seed test "serves as an example of all the generated tests" — that applies here.
>
> **If a spec listed below has moved or been renamed**, grep `tests/` for a remaining example (e.g. `grep -rn "loadSeriesByModality('RTSTRUCT')" tests/`). The pattern matters more than the exact filename — specs get renamed, the feature area persists.
>
> **No close match below?** This list only covers areas with a seed worth copying; don't add a stub for every untested area. See [Feature area not listed above?](#feature-area-not-listed-above-no-existing-seed).
## Canonical study UIDs
| UID | Mode(s) | What it has |
|-----|---------|-------------|
| `1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5` | `viewer` | CT — default for measurement/annotation tests |
| `1.3.6.1.4.1.14519.5.2.1.1706.8374.643249677828306008300337414785` | `viewer` | CT volume for 3D/MPR/crosshairs |
| `1.3.6.1.4.1.14519.5.2.1.256467663913010332776401703474716742458` | `viewer` or `segmentation` | CT + SEG for labelmap tests |
| `1.2.840.113619.2.290.3.3767434740.226.1600859119.501` | `viewer` / `segmentation` / `tmtv` | CT + RTSTRUCT + PET, used for contour and TMTV |
| `1.3.6.1.4.1.14519.5.2.1.7695.4007.324475281161490036195179843543` | `viewer` | SR structured report |
Do not invent UIDs — they must exist on the e2e data server.
---
## 1. Simple measurement tools (length, angle, bidirectional, rectangle, ellipse, circle)
**Seed:** `tests/Length.spec.ts`, `tests/Angle.spec.ts`
Pattern: select tool via `mainToolbarPageObject.measurementTools.<tool>.click()`, place N points via `activeViewport.normalizedClickAt([...])`, confirm the tracking prompt, screenshot via `screenShotPaths.<name>.<name>DisplayedCorrectly`.
## 2. Freehand / spline / livewire ROIs
**Seed:** `tests/FreehandROI.spec.ts`, `tests/Livewire.spec.ts`, `tests/Spline.spec.ts`
Pattern: `normalizedDragAt({ start, end, config: { steps: 20, delay: 30 } })` for smooth strokes; `subscribeToMeasurementAdded` to assert the event fires; `activeViewport.nthAnnotation(0)` to reference what was drawn.
## 3. Annotations (arrow, probe)
**Seed:** `tests/ArrowAnnotate.spec.ts`, `tests/Probe.spec.ts`
Arrow annotate opens `DOMOverlayPageObject.dialog.input` for the label. Use `fillAndSave(label)`.
## 4. Measurement panel interactions
**Seed:** `tests/MeasurementPanel.spec.ts`
Panel access: `rightPanelPageObject.toggle()``.measurementsPanel.select()``.panel.nthMeasurement(i)``.actions.rename|delete|toggleLock|duplicate|...`. Also demonstrates `addLengthMeasurement(page)` and panel-row `click()` for jump-to.
## 5. Context menu (right-click on annotation)
**Seed:** `tests/ContextMenu.spec.ts`
Two ways to open: `activeViewport.normalizedClickAt([{...}], 'right')` on an empty area, or `activeViewport.nthAnnotation(0).contextMenu.open()` on a drawn annotation. Then `DOMOverlayPageObject.viewport.annotationContextMenu.addLabel|delete.click()`.
## 6. Labelmap segmentation (SEG) hydration
**Seed:** `tests/SEGHydration.spec.ts`, `tests/SEGHydrationThenMPR.spec.ts`
Flow: `leftPanelPageObject.loadSeriesByModality('SEG')``waitForTimeout(3000)``DOMOverlayPageObject.viewport.segmentationHydration.yes.click()`. Often pokes Cornerstone state directly via `page.evaluate(() => window.cornerstone...)` for zoom/render.
## 7. Contour segmentation (RTSTRUCT) + interactions
**Seeds:**
- Hydration: `tests/RTHydration.spec.ts`
- Rename: `tests/ContourSegmentRename.spec.ts`
- Navigation between segments: `tests/ContourSegNavigation.spec.ts`
- Duplicate: `tests/ContourSegmentDuplicate.spec.ts`
- Visibility: `tests/ContourSegmentToggleVisibility.spec.ts`
- Locking: `tests/ContourSegLocking.spec.ts`
Pattern: load RTSTRUCT series → hydrate → use `rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(i)` / `.segmentByText('Small Sphere')` and their `.actions.rename|delete`, or the segment's `.click()` to jump.
## 8. Labelmap segmentation editing (brush / eraser / threshold)
**Seed:** `tests/LabelMapSegLocking.spec.ts`, `tests/SEGDrawingToolsResizing.spec.ts`
Pattern: `rightPanelPageObject.labelMapSegmentationPanel.tools.brush.setRadius(n)` / `.click()`, then `activeViewport.normalizedDragAt(...)` to paint.
## 9. TMTV / PET
**Seeds:** `tests/TMTVCSVReport.spec.ts`, `tests/TMTVSUV.spec.ts`, `tests/TMTVAlignment.spec.ts`, `tests/TMTVRendering.spec.ts`
Visit `mode: 'tmtv'` with a longer delay (`10000`). `rightPanelPageObject.tmtvPanel` for the side panel. Exporting a report uses `page.waitForEvent('download')` + `downloadAsString(download)`.
## 10. MPR and 3D layouts
**Seeds:** `tests/MPR.spec.ts`, `tests/3DOnly.spec.ts`, `tests/3DFourUp.spec.ts`, `tests/3DMain.spec.ts`, `tests/AxialPrimary.spec.ts`
Pattern: `mainToolbarPageObject.layoutSelection.<layoutName>.click()`. For 3D, wrap stabilization with `attemptAction(() => reduce3DViewportSize(page), 10, 100)` to settle the render before asserting.
## 11. Crosshairs
**Seed:** `tests/Crosshairs.spec.ts`
Pattern: `initializeMousePositionTracker(page)` in `beforeEach`, `mainToolbarPageObject.crosshairs.click()`, then `viewportPageObject.crosshairs.axial.rotate()` / `.increase()`.
## 12. Overlays — data overlay menu, window/level, orientation
**Seeds:** `tests/DataOverlayMenu.spec.ts`, `tests/WindowLevelOverlayText.spec.ts`, `tests/MultipleSegmentationDataOverlays.spec.ts`
Pattern: `viewportPageObject.getById('default').overlayMenu.dataOverlay.toggle()`, then `.addSegmentation(name)` / `.changeSegmentation(from, to)` / `.remove(name)`. For keyboard navigation, remember `press({ page, key, nTimes })` imports from `./utils/keyboardUtils`.
## 13. DICOM Tag Browser
**Seed:** `tests/DicomTagBrowser.spec.ts`
Open with `mainToolbarPageObject.moreTools.tagBrowser.click()`, then interact via `DOMOverlayPageObject.dialog.dicomTagBrowser.waitVisible()` / `.seriesSelect.selectOption(i)` / `.seriesSelect.getOptionText(i)`.
## 14. Study validation / not-found / worklist
**Seeds:** `tests/StudyValidation.spec.ts`, `tests/Worklist.spec.ts`
These are the rare specs that use `page.goto(...)` directly instead of `visitStudy()` — because they test error states or non-study pages. `notFoundStudyPageObject` gives you `errorMessage`, `returnMessage`, `studyListLink`.
## 15. SR (Structured Report) hydration
**Seeds:** `tests/SRHydration.spec.ts`, `tests/SRHydrationThenReload.spec.ts`
Same shape as SEG hydration but load via `loadSeriesByModality('SR')`.
## Feature area not listed above? (no existing seed)
Don't add a stub section here for an untested area. When your target isn't listed:
1. **Look for an indirect seed.** Grep `tests/` for the controls or flow you need
(`grep -rn "options-menu" tests/`, a related panel/dialog). An adjacent area's seed
usually still shows how the harness is driven.
2. **If genuinely uncovered, you're *creating* page objects.** Read
[page-objects.md](page-objects.md) → "When the control you need isn't covered yet", then:
- One page object per new control. Dialogs follow `DicomTagBrowserPageObject` (reached
through `DOMOverlayPageObject`); toolbar buttons get a getter on `MainToolbarPageObject`.
- Add a `data-cy` to any control that lacks one.
- **Verify the real effect, not a proxy** — drag and screenshot that the image moved via
`locator: viewportPageObject.grid`, not just that a button gained `data-active`.
**Example — user preferences / hotkeys (no seed today):** open the options menu and User
Preferences dialog (options-menu accessor on `DOMOverlayPageObject` + a `userPreferences`
dialog page object reached through it), set the hotkey, save, then trigger it and confirm the
viewport effect (activate Zoom, drag, screenshot the zoom). Add the Zoom button as a getter on
`MainToolbarPageObject`.
---
## Advanced patterns worth knowing
- **`subscribeToMeasurementAdded`** (`tests/FreehandROI.spec.ts`) — for async "was a measurement actually added?" assertions. Always `try { ... } finally { await measurementAdded.unsubscribe() }`.
- **`attemptAction`** (`tests/3DOnly.spec.ts`) — retry flaky setup (3D render, heavy layout change) without silencing real failures.
- **`addOHIFConfiguration`** (`tests/RTHydrationDisableConfirmation.spec.ts`) — pre-load config overrides before `visitStudy`.
- **`page.evaluate(() => window.services...)`** — used in several SEG/SR specs to set customizations or poke viewport state. Treat as an escape hatch, not a default.
- **`expect.toPass({ timeout })`** — wrap flaky assertions (common for jump-to-measurement tests where rendering settles asynchronously).

View File

@ -0,0 +1,100 @@
# OHIF Test Utility guide
> This file documents the **stable import rules and conventions** around `tests/utils/`. For the current list of exported helpers and their exact signatures, **read `tests/utils/index.ts` and the files it re-exports** — the barrel is always current; a static table here is not. Utilities get added, renamed, and refactored; the rules below change much more slowly.
## How to discover what's available
1. Open `tests/utils/index.ts`. Every symbol exported from the barrel is importable as `import { foo } from './utils'`.
2. If what you need isn't in the barrel, look in the rest of `tests/utils/` — there are a handful of specialized files (`keyboardUtils.ts`, `assertions.ts`, `download.ts`, …). These need the **deeper import path**; see the rule below.
3. For the actual signature, read the utility's `.ts` file. It's one short function per file in most cases.
4. To see a utility in context, grep `tests/` (`grep -rn visitStudy tests/`) — or open the seed spec for the relevant feature area ([patterns-by-feature.md](patterns-by-feature.md)). Existing specs are the most reliable signature reference because they co-evolve with the API.
Do not guess parameter shapes from memory, and do not treat this file as an API catalog — it intentionally isn't one.
---
## Stable rules
### Barrel vs. deep imports
Two import styles exist. They are not interchangeable.
```ts
// Barrel — anything re-exported from tests/utils/index.ts
import { test, expect, visitStudy, checkForScreenshot, screenShotPaths } from './utils';
// Deep — for files NOT re-exported by the barrel
import { press } from './utils/keyboardUtils';
import { assertNumberOfModalityLoadBadges } from './utils/assertions';
import { downloadAsString } from './utils/download';
```
If a symbol isn't in the barrel, `import { x } from './utils'` **compiles**, resolves `x` to `undefined`, and blows up at the first call site. Confirm by opening `tests/utils/index.ts` for your working revision. At the time of this writing, `press`, `downloadAsString`, and the `assert*` helpers live outside the barrel — but maintainers can move things in or out, so treat `index.ts` as the ground truth rather than this note.
### Never import `test` / `expect` from `@playwright/test`
```ts
// ✅ Correct — gets the fixture-extended runner
import { test, expect } from './utils';
// ❌ Wrong — compiles, but every page-object fixture is silently undefined
import { test, expect } from '@playwright/test';
```
If your test function's destructured arguments (like `viewportPageObject`) are `undefined`, this import is almost always why.
### `visitStudy` — 2000 ms is a convention, not a default
```ts
await visitStudy(page, studyInstanceUID, mode, 2000);
```
The function's own default delay is `0`. Nearly every spec passes `2000` to let the first render settle. The one consistent exception is `mode: 'tmtv'`, where the suite uniformly uses `10000` because PET fusion and SUV calculation add real wall-clock cost before the UI is interactive.
Notably, 3D layouts in `viewer` mode (3DOnly, 3DFourUp, 3DMain, 3DPrimary) and MPR also use `2000` — they're not "heavy" in the `visitStudy` sense. Their stabilization problem is solved at the interaction layer with `attemptAction(() => reduce3DViewportSize(page), 10, 100)`, not by a longer visit delay.
So: pick `10000` when the mode is `tmtv`, `2000` otherwise, and only ramp up if a specific test flakes on first-render assertions. The delay is a good first lever for "not visible" flakes, but it's not a universal upgrade.
### `checkForScreenshot` — use the object form, never screenshot the full app
**This is the direction going forward** The suite is being migrated off *full-app* screenshots — not off screenshots altogether. Screenshots are the correct and required tool whenever what you're verifying is canvas-only raster output with no DOM signal; don't avoid them there. Avoid them only where a faithful DOM/SVG signal exists (e.g. a vector overlay's color via `getSvgAttribute`) or where you'd be capturing the whole app. See SKILL.md → "Screenshot vs. DOM assertion — how to choose". Any new spec must follow the rules below, and any modification to an older spec should bring it in line when reasonable.
- **Object form** (required for all new specs): `checkForScreenshot({ page, screenshotPath, normalizedClip?, ... })`
- **Positional form**: legacy. It still appears in older specs because they haven't been migrated yet. **Do not treat existing positional-form usage as a pattern to copy** — those specs are the thing being moved away from. Do not introduce the positional form in new code.
**Hard rules for new screenshots:**
1. Use the object form.
2. Scope by passing a `locator``viewportPageObject.grid` for the whole grid, or a specific viewport pane locator. **Never screenshot the full app.** `normalizedClip` is computed *relative to the locator* (and defaults to the full page when no locator is given), so `{ x: 0, y: 0, width: 1, height: 1 }` alone does not scope anything — reserve `normalizedClip` for clipping to a sub-region of a locator. If you find yourself reaching for `fullPage: true`, stop and pass a locator instead.
Do not tune `maxDiffPixelRatio` or `threshold` to make a screenshot pass — those are intentionally rarely touched and not the right knob for flakes. If a baseline mismatches, regenerate it (`--update-snapshots`) after a human review of the diff, or fix the underlying instability. Check the current signature in `tests/utils/checkForScreenshot.ts` if something looks off.
### `screenShotPaths` — use keys, not raw strings
```ts
await checkForScreenshot({
page,
locator: viewportPageObject.grid,
screenshotPath: screenShotPaths.length.lengthDisplayedCorrectly,
});
```
The full tree of categories lives in `tests/utils/screenShotPaths.ts`. When you need a new baseline, **add the key there and reference it by name** rather than typing a raw path. A typo in a key becomes a compile error instead of a silent mismatch, and other tests become discoverable through the object.
### Object-param convention
Many OHIF test utilities take a single **object argument** rather than positional arguments — notably `press({ page, key, nTimes? })`, the `simulate*` helpers, and the `assert*` helpers. If a call looks like it should work but throws "cannot read properties of undefined," check whether you're passing positional args to something that expects `{ page, ... }`.
Read the one-line signature at the top of the utility's source file before calling it — it's faster than guessing, and it's always right.
---
## Utility shapes worth flagging
Most utilities are obvious once you read the source; these earn a mention:
- **`waitForViewportRenderCycle(page, options?)`** — preferred replacement for `page.waitForTimeout(...)` after any viewport-mutating action (hydration confirm, layout change, segmentation add, series load, etc.). Start it **before** the action, await it after — it captures the `needsRender → rendered` transition the action triggers. Use `waitForViewportsRendered(page)` (the second-half-only variant) when the render is already in flight before you can attach a watcher. Source: `tests/utils/waitForViewportsRendered.ts`. Seed: `tests/SEGHydrationFromMPR.spec.ts`. The "Wait for renders, don't sleep" section in [SKILL.md](../SKILL.md) covers the idiom in full.
- **`subscribeToMeasurementAdded(page)`** — returns `{ waitFired(timeout?), unsubscribe() }`. Use in freehand/livewire/spline specs to assert the event fired. Always wrap in `try { ... } finally { await sub.unsubscribe() }` so a failing assertion doesn't leak the listener across tests.
- **`attemptAction(action, attempts?, delay?)`** — retries a flaky async action without masking real failures. Mainly used to stabilize 3D scenes (`attemptAction(() => reduce3DViewportSize(page), 10, 100)`).
For everything else, the pattern is: find a spec that uses it (see [patterns-by-feature.md](patterns-by-feature.md)), copy the shape, adapt.

View File

@ -1,26 +1,30 @@
version: 2.1 version: 2.1
# ci: re-trigger pipeline
orbs: orbs:
codecov: codecov/codecov@1.0.5 codecov: codecov/codecov@1.0.5
cypress: cypress-io/cypress@3.4.2 cypress: cypress-io/cypress@3.4.2
defaults: &defaults defaults: &defaults
docker: docker:
- image: cimg/node:20.18.1 - image: cimg/node:24.15.0
environment: environment:
TERM: xterm TERM: xterm
QUICK_BUILD: true QUICK_BUILD: true
working_directory: ~/repo working_directory: ~/repo
commands: commands:
install_bun: install_pnpm:
steps: steps:
- run: - run:
name: Install Bun name: Install pnpm
command: | command: |
curl -fsSL https://bun.sh/install | bash -s "bun-v1.2.23" # The cimg/node global modules dir (/usr/local/lib/node_modules) is
echo 'export BUN_INSTALL="$HOME/.bun"' >> $BASH_ENV # root-owned, so a plain `npm install -g` fails with EACCES. Install
echo 'export PATH="$BUN_INSTALL/bin:$PATH"' >> $BASH_ENV # with sudo so the global pnpm binary lands in the shared prefix.
sudo npm install -g pnpm@11.5.2
echo 'export PATH="$(pnpm store path)/../.bin:$PATH"' >> $BASH_ENV
source $BASH_ENV source $BASH_ENV
jobs: jobs:
@ -28,16 +32,16 @@ jobs:
<<: *defaults <<: *defaults
resource_class: large resource_class: large
steps: steps:
- install_bun - install_pnpm
- run: node --version - run: node --version
- checkout - checkout
- run: - run:
name: Install Dependencies name: Install Dependencies
command: bun install --no-save command: pnpm install --frozen-lockfile
# RUN TESTS # RUN TESTS
- run: - run:
name: 'JavaScript Test Suite' name: 'JavaScript Test Suite'
command: bun run test:unit:ci command: pnpm run test:unit:ci
# platform/app # platform/app
- run: - run:
name: 'VIEWER: Combine report output' name: 'VIEWER: Combine report output'
@ -71,17 +75,17 @@ jobs:
steps: steps:
# Checkout code and ALL Git Tags # Checkout code and ALL Git Tags
- checkout - checkout
- install_bun - install_pnpm
- run: - run:
name: Install Dependencies name: Install Dependencies
command: bun install --no-save command: pnpm install --frozen-lockfile
# Build & Test # Build & Test
- run: - run:
name: 'Perform the versioning before build' name: 'Perform the versioning before build'
command: bun ./version.mjs command: node ./version.mjs
- run: - run:
name: 'Build the OHIF Viewer' name: 'Build the OHIF Viewer'
command: bun run build command: pnpm run build
no_output_timeout: 45m no_output_timeout: 45m
- run: - run:
name: 'Upload SourceMaps, Send Deploy Notification' name: 'Upload SourceMaps, Send Deploy Notification'
@ -107,58 +111,55 @@ jobs:
<<: *defaults <<: *defaults
resource_class: large resource_class: large
steps: steps:
- install_bun - install_pnpm
# Checkout code and ALL Git Tags # Checkout code and ALL Git Tags
- checkout - checkout
- attach_workspace: - attach_workspace:
at: ~/repo at: ~/repo
# SECURITY AUDIT # SECURITY AUDIT - only when pnpm-lock.yaml has changed
- run: - run:
name: 'Security Audit - High Risk Vulnerabilities' name: 'Security Audit - High Risk Vulnerabilities'
command: | command: |
echo "🔍 Running bun audit for security vulnerabilities..." git fetch origin master 2>/dev/null || true
BASE_REF=$(git merge-base HEAD origin/master 2>/dev/null)
if [[ -z "$BASE_REF" ]]; then
echo "Could not determine base ref (e.g. shallow clone or no origin/master), skipping security audit."
exit 0
fi
CHANGED_FILES=$(git diff --name-only origin/master...HEAD 2>/dev/null || echo "")
if ! echo "$CHANGED_FILES" | grep -qx 'pnpm-lock.yaml'; then
echo "pnpm-lock.yaml unchanged - skipping security audit."
exit 0
fi
echo "pnpm-lock.yaml changed - running pnpm audit for security vulnerabilities..."
echo "Checking for HIGH-RISK vulnerabilities..." echo "Checking for HIGH-RISK vulnerabilities..."
# Define ignored vulnerabilities with comments if pnpm audit --audit-level high; then
IGNORED_VULNS=( echo "No high-risk vulnerabilities found"
"GHSA-5j98-mcp5-4vw2" # CVE-2025-64756 - glob is not used on the command line echo "Security audit passed!"
"GHSA-8qq5-rm4j-mr97" # CVE-2026-23745 - limited to build/dev environments
"GHSA-r6q2-hw4h-h46w" # CVE-2026-23950 - limited to build/dev environments
"GHSA-34x7-hfp2-rc4v" # CVE-2026-24842 - limited to build/dev environments
)
# Build ignore flags
IGNORE_FLAGS=""
for vuln in "${IGNORED_VULNS[@]}"; do
IGNORE_FLAGS="$IGNORE_FLAGS --ignore=$vuln"
done
if bun audit $IGNORE_FLAGS --audit-level high; then
echo "✅ No high-risk vulnerabilities found"
echo "🎉 Security audit passed!"
else else
echo "" echo ""
echo "HIGH-RISK VULNERABILITIES DETECTED!" echo "HIGH-RISK VULNERABILITIES DETECTED!"
echo "======================================" echo "======================================"
echo "" echo ""
echo "🔧 To fix these issues:" echo "To fix these issues:"
echo " 1. Run: bun audit" echo " 1. Run: pnpm audit"
echo " 2. Review the vulnerability details" echo " 2. Review the vulnerability details"
echo " 3. Update affected packages to secure versions" echo " 3. Update affected packages to secure versions"
echo " 4. Test your changes" echo " 4. Test your changes"
echo " 5. Re-run: bun audit --audit-level high" echo " 5. Re-run: pnpm audit --audit-level high"
echo "" echo ""
echo "📋 Full audit report:" echo "Full audit report:"
bun audit $IGNORE_FLAGS --audit-level low || true pnpm audit || true
echo "" echo ""
echo "This build cannot proceed until high-risk vulnerabilities are resolved." echo "This build cannot proceed until high-risk vulnerabilities are resolved."
exit 1 exit 1
fi fi
- run: - run:
name: Install Dependencies name: Install Dependencies
command: bun install --frozen-lockfile command: pnpm install
- run: - run:
name: Avoid hosts unknown for github name: Avoid hosts unknown for github
command: | command: |
@ -169,28 +170,34 @@ jobs:
git config --global user.name "ohif-bot" git config --global user.name "ohif-bot"
- run: - run:
name: Authenticate with NPM registry name: Authenticate with NPM registry
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc # Write the auth token to npm's per-user config (~/.npmrc), not the
# repo root. publish-package.mjs chdir's into each package dir to run
# `npm publish`, and npm only reads the project .npmrc from that dir
# plus the user .npmrc from $HOME -- it never sees ~/repo/.npmrc, so a
# root-level token yields ENEEDAUTH. Using ~/.npmrc also avoids
# clobbering the committed workspace-config .npmrc (node-linker=hoisted).
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
- run: - run:
name: build half of the packages (to avoid out of memory in circleci) name: build half of the packages (to avoid out of memory in circleci)
command: | command: |
bun run build:package-all pnpm run build:package-all
- run: - run:
name: build the other half of the packages name: build the other half of the packages
command: | command: |
bun run build:package-all-1 pnpm run build:package-all-1
NPM_PUBLISH: NPM_PUBLISH:
<<: *defaults <<: *defaults
resource_class: large resource_class: large
steps: steps:
- install_bun - install_pnpm
# Checkout code and ALL Git Tags # Checkout code and ALL Git Tags
- checkout - checkout
- attach_workspace: - attach_workspace:
at: ~/repo at: ~/repo
- run: - run:
name: Install Dependencies name: Install Dependencies
command: bun install --no-save command: pnpm install --frozen-lockfile
- run: - run:
name: Avoid hosts unknown for github name: Avoid hosts unknown for github
command: | command: |
@ -201,15 +208,21 @@ jobs:
git config --global user.name "ohif-bot" git config --global user.name "ohif-bot"
- run: - run:
name: Authenticate with NPM registry name: Authenticate with NPM registry
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc # Write the auth token to npm's per-user config (~/.npmrc), not the
# repo root. publish-package.mjs chdir's into each package dir to run
# `npm publish`, and npm only reads the project .npmrc from that dir
# plus the user .npmrc from $HOME -- it never sees ~/repo/.npmrc, so a
# root-level token yields ENEEDAUTH. Using ~/.npmrc also avoids
# clobbering the committed workspace-config .npmrc (node-linker=hoisted).
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
- run: - run:
name: build half of the packages (to avoid out of memory in circleci) name: build half of the packages (to avoid out of memory in circleci)
command: | command: |
bun run build:package-all pnpm run build:package-all
- run: - run:
name: build the other half of the packages name: build the other half of the packages
command: | command: |
bun run build:package-all-1 pnpm run build:package-all-1
- run: - run:
name: increase min time out name: increase min time out
command: | command: |
@ -221,14 +234,20 @@ jobs:
- run: - run:
name: publish package versions name: publish package versions
command: | command: |
bun ./publish-version.mjs node ./publish-version.mjs
- run: - run:
name: Again set the NPM registry (was deleted in the version script) name: Re-assert the NPM auth token before publishing
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc # Write the auth token to npm's per-user config (~/.npmrc), not the
# repo root. publish-package.mjs chdir's into each package dir to run
# `npm publish`, and npm only reads the project .npmrc from that dir
# plus the user .npmrc from $HOME -- it never sees ~/repo/.npmrc, so a
# root-level token yields ENEEDAUTH. Using ~/.npmrc also avoids
# clobbering the committed workspace-config .npmrc (node-linker=hoisted).
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
- run: - run:
name: publish package dist name: publish package dist
command: | command: |
bun ./publish-package.mjs node ./publish-package.mjs
- persist_to_workspace: - persist_to_workspace:
root: ~/repo root: ~/repo
paths: paths:
@ -250,8 +269,11 @@ jobs:
if [[ ! -e version.txt ]]; then if [[ ! -e version.txt ]]; then
exit 0 exit 0
else else
# Remove npm config # Restore the committed .npmrc (pnpm workspace config such as
rm -f ./.npmrc # node-linker=hoisted). NPM_PUBLISH overwrote it with a publish
# auth token, but the Dockerfile COPYs .npmrc for the in-image
# pnpm install, so it needs the config file present and token-free.
git checkout -- .npmrc
# Set our version number using vars # Set our version number using vars
export IMAGE_VERSION=$(cat version.txt) export IMAGE_VERSION=$(cat version.txt)
export IMAGE_VERSION_FULL=v$IMAGE_VERSION export IMAGE_VERSION_FULL=v$IMAGE_VERSION
@ -284,8 +306,11 @@ jobs:
if [[ ! -e version.txt ]]; then if [[ ! -e version.txt ]]; then
exit 0 exit 0
else else
# Remove npm config # Restore the committed .npmrc (pnpm workspace config such as
rm -f ./.npmrc # node-linker=hoisted). NPM_PUBLISH overwrote it with a publish
# auth token, but the Dockerfile COPYs .npmrc for the in-image
# pnpm install, so it needs the config file present and token-free.
git checkout -- .npmrc
# Set our version number using vars # Set our version number using vars
export IMAGE_VERSION=$(cat version.txt) export IMAGE_VERSION=$(cat version.txt)
export IMAGE_VERSION_FULL=v$IMAGE_VERSION export IMAGE_VERSION_FULL=v$IMAGE_VERSION
@ -322,7 +347,11 @@ jobs:
exit 0 exit 0
else else
echo "Building and pushing Docker image from the master branch (beta releases)" echo "Building and pushing Docker image from the master branch (beta releases)"
rm -f ./.npmrc # Restore the committed .npmrc (pnpm workspace config such as
# node-linker=hoisted). NPM_PUBLISH overwrote it with a publish
# auth token, but the Dockerfile COPYs .npmrc for the in-image
# pnpm install, so it needs the config file present and token-free.
git checkout -- .npmrc
# Set our version number using vars # Set our version number using vars
export IMAGE_VERSION=$(cat version.txt) export IMAGE_VERSION=$(cat version.txt)
@ -356,7 +385,11 @@ jobs:
exit 0 exit 0
else else
echo "Building and pushing ARM64 Docker image from the master branch (beta releases)" echo "Building and pushing ARM64 Docker image from the master branch (beta releases)"
rm -f ./.npmrc # Restore the committed .npmrc (pnpm workspace config such as
# node-linker=hoisted). NPM_PUBLISH overwrote it with a publish
# auth token, but the Dockerfile COPYs .npmrc for the in-image
# pnpm install, so it needs the config file present and token-free.
git checkout -- .npmrc
# Set our version number using vars # Set our version number using vars
export IMAGE_VERSION=$(cat version.txt) export IMAGE_VERSION=$(cat version.txt)
export IMAGE_VERSION_FULL=v$IMAGE_VERSION export IMAGE_VERSION_FULL=v$IMAGE_VERSION
@ -374,11 +407,21 @@ jobs:
resource_class: large resource_class: large
parallelism: 8 parallelism: 8
steps: steps:
- install_pnpm
- run: - run:
name: Install System Dependencies name: Install System Dependencies
command: | command: |
sudo apt-get update # CircleCI's base image registers third-party apt sources (git-lfs via
sudo apt-get install -y xvfb libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 # packagecloud, git-core PPA via launchpad) that periodically hang
# `apt-get update`. The Cypress libs below all come from the Ubuntu
# archive, so drop those sources and bound the update with timeouts +
# retries so a slow mirror can't stall the job indefinitely.
sudo rm -f /etc/apt/sources.list.d/*git* || true
sudo apt-get update \
-o Acquire::Retries=3 \
-o Acquire::http::Timeout=20 \
-o Acquire::https::Timeout=20
sudo apt-get install -y xvfb libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6
- run: - run:
name: Start Xvfb name: Start Xvfb
command: Xvfb :99 -screen 0 1920x1080x24 & command: Xvfb :99 -screen 0 1920x1080x24 &
@ -387,11 +430,15 @@ jobs:
name: Export Display Variable name: Export Display Variable
command: export DISPLAY=:99 command: export DISPLAY=:99
- cypress/install: - cypress/install:
install-command: yarn install --frozen-lockfile --no-save install-command: pnpm install
- cypress/run-tests: - cypress/run-tests:
# CI runs headless under Xvfb with no GPU, so Electron uses software
# WebGL. Newer Chromium deprecated that implicit fallback (canvas
# rendering degrades and races with element clicks). ELECTRON_EXTRA_LAUNCH_ARGS
# is the reliable way to pass the opt-in flag to Cypress's Electron browser.
cypress-command: | cypress-command: |
npx wait-on@latest http://localhost:3000 && cd platform/app && npx cypress run --record --parallel npx wait-on@latest http://localhost:3000 && cd platform/app && ELECTRON_EXTRA_LAUNCH_ARGS="--enable-unsafe-swiftshader" npx cypress run --record --parallel
start-command: yarn run test:data && yarn run test:e2e:serve start-command: pnpm run test:data && pnpm run test:e2e:serve
DOCKER_MULTIARCH_MANIFEST: DOCKER_MULTIARCH_MANIFEST:
<<: *defaults <<: *defaults

View File

@ -1,15 +1,9 @@
version: 2 version: 2
enable-beta-ecosystems: true enable-beta-ecosystems: true
updates: updates:
- package-ecosystem: 'bun'
directory: '/'
schedule:
interval: 'daily'
labels: ['dependencies']
commit-message:
prefix: 'chore'
include: 'scope'
- package-ecosystem: 'npm' - package-ecosystem: 'npm'
# Disable all pull requests for npm/pnpm version updates.
open-pull-requests-limit: 0
directory: '/' directory: '/'
schedule: schedule:
interval: 'daily' interval: 'daily'

View File

@ -21,16 +21,29 @@ jobs:
contents: read contents: read
pull-requests: read pull-requests: read
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: oven-sh/setup-bun@v2
with: with:
bun-version: 1.2.23 persist-credentials: false
- uses: actions/setup-node@v4 - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: 20 # Or your desired Node version node-version: 24.15.0
cache: pnpm
- name: Install root dependencies - name: Configure git for private repos
run: bun install --frozen-lockfile run: git config --global url."https://${GITHUB_TOKEN}:x-oauth-basic@github.com/".insteadOf ssh://git@github.com/
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
# Internal @ohif/* deps are workspace:* in the committed manifests and the
# lockfile records them as links, so pnpm-lock.yaml stays in sync across
# version bumps and a frozen install passes. Frozen is the right choice
# for a deploy job: it fails fast if a PR ever changed a real dependency
# without committing the lockfile update, instead of silently reconciling.
# (Exact versions appear only in published tarballs, where pnpm publish
# rewrites workspace:* at pack time -- not publish-version.mjs.)
run: pnpm install --frozen-lockfile
# Removed Playwright tests and coverage generation steps # Removed Playwright tests and coverage generation steps
@ -44,47 +57,44 @@ jobs:
# Note: This relies on the merge commit being directly pushed to main # Note: This relies on the merge commit being directly pushed to main
PR_DATA=$(gh pr list --state merged --search "$MERGE_COMMIT_SHA" --json number,headRefOid --jq '.[0]') PR_DATA=$(gh pr list --state merged --search "$MERGE_COMMIT_SHA" --json number,headRefOid --jq '.[0]')
if [ -z "$PR_DATA" ]; then if [ -z "$PR_DATA" ]; then
echo "Could not find merged PR for commit $MERGE_COMMIT_SHA." echo "::warning::Could not find merged PR for commit $MERGE_COMMIT_SHA. Skipping coverage embedding."
# Decide how to handle - fail, or maybe generate coverage now? echo "coverage_found=false" >> $GITHUB_OUTPUT
# For now, let's fail. exit 0
exit 1
fi fi
PR_HEAD_SHA=$(echo "$PR_DATA" | jq -r '.headRefOid') PR_HEAD_SHA=$(echo "$PR_DATA" | jq -r '.headRefOid')
PR_NUMBER=$(echo "$PR_DATA" | jq -r '.number') PR_NUMBER=$(echo "$PR_DATA" | jq -r '.number')
echo "Found PR Number: $PR_NUMBER" echo "Found PR Number: $PR_NUMBER"
echo "Found PR Head SHA: $PR_HEAD_SHA" echo "Found PR Head SHA: $PR_HEAD_SHA"
# Find the latest workflow run ID for the playwright workflow on the PR head commit # Find the latest *successful* playwright.yml run for the PR head commit so we
# Uses the workflow file name 'playwright.yml' # never embed coverage from a failed/incomplete run.
# Remove the --status success flag to find any run RUN_ID=$(gh run list --workflow playwright.yml --commit "$PR_HEAD_SHA" --event pull_request --status success --json databaseId --jq '.[0].databaseId')
RUN_ID=$(gh run list --workflow playwright.yml --commit "$PR_HEAD_SHA" --event pull_request --json databaseId --jq '.[0].databaseId')
if [ -z "$RUN_ID" ]; then if [ -z "$RUN_ID" ]; then
echo "Could not find any 'playwright.yml' run for PR $PR_NUMBER (Head SHA: $PR_HEAD_SHA)." echo "::warning::Could not find a successful 'playwright.yml' run for PR $PR_NUMBER (Head SHA: $PR_HEAD_SHA). Skipping coverage embedding."
# Decide how to handle - maybe try finding the artifact from the merge commit run if that exists? echo "coverage_found=false" >> $GITHUB_OUTPUT
# For now, let's fail. exit 0
exit 1
fi fi
echo "Found Run ID: $RUN_ID" echo "Found Run ID: $RUN_ID"
echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT
echo "coverage_found=true" >> $GITHUB_OUTPUT
- name: Download coverage artifact from PR run - name: Download coverage artifact from PR run
if: steps.find_pr_run.outputs.coverage_found == 'true'
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
run: | run: |
mkdir -p ./coverage-artifact mkdir -p ./coverage-artifact
gh run download ${{ steps.find_pr_run.outputs.run_id }} -n coverage-report-pr --dir ./coverage-artifact gh run download ${{ steps.find_pr_run.outputs.run_id }} -n coverage-report-pr --dir ./coverage-artifact
# Check if download was successful (e.g., check if files exist) # Verify the artifact contains an HTML coverage report rather than checking a single asset
if [ ! -f ./coverage-artifact/base.css ]; then if [ -z "$(ls -A ./coverage-artifact)" ] || ! ls ./coverage-artifact/*.html >/dev/null 2>&1; then
echo "Failed to download or find expected files in artifact 'coverage-report-pr' from run ${{ steps.find_pr_run.outputs.run_id }}." echo "Failed to download or find an HTML coverage report in artifact 'coverage-report-pr' from run ${{ steps.find_pr_run.outputs.run_id }}."
exit 1 exit 1
fi fi
echo "Artifact downloaded successfully." echo "Artifact downloaded successfully."
- name: Install docs dependencies
run: cd platform/docs && bun install
- name: Copy coverage to docs static directory - name: Copy coverage to docs static directory
if: steps.find_pr_run.outputs.coverage_found == 'true'
run: | run: |
# Copy files from the downloaded artifact directory # Copy files from the downloaded artifact directory
mkdir -p platform/docs/static/coverage mkdir -p platform/docs/static/coverage
@ -99,12 +109,17 @@ jobs:
cp ./coverage-artifact/sorter.js platform/docs/static/ cp ./coverage-artifact/sorter.js platform/docs/static/
- name: Build docs - name: Build docs
run: cd platform/docs && bun run build run: pnpm --filter ohif-docs run build
- name: Deploy to Netlify - name: Deploy to Netlify
run: | # The docs are already built by the "Build docs" step above, so pass
cd platform/docs # --no-build: `netlify deploy` runs the build command by default, which
npx netlify-cli deploy --dir=./build --prod # would otherwise pick up the repo-root netlify.toml (configured for the
# main viewer app) and build the wrong project. --dir is resolved
# relative to the repo root (the netlify.toml base), not the cwd, so it
# must be the full path to the prebuilt docs output. --filter selects
# the ohif-docs package so Netlify CLI does not abort on monorepo detection.
run: npx netlify-cli deploy --filter ohif-docs --dir=platform/docs/build --prod --no-build
env: env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}

View File

@ -55,7 +55,9 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
# Add any setup steps before running the `github/codeql-action/init` action. # Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node` # This includes steps like installing compilers or runtimes (`actions/setup-node`
@ -65,7 +67,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3.36.2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@ -93,6 +95,6 @@ jobs:
exit 1 exit 1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3.36.2
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@ -2,6 +2,19 @@ name: Playwright Tests
on: on:
pull_request: pull_request:
branches: [master, release/*] branches: [master, release/*]
workflow_dispatch:
inputs:
cs3d_ref:
description: >-
CS3D branch (e.g. main, origin:feat/foo) or version (e.g. 4.18.2, 4.19+, 4.x). Only used
when ohif-integration label is present or via workflow_dispatch.
required: false
default: '4.19+'
permissions:
contents: read
pull-requests: read
issues: read
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@ -9,50 +22,211 @@ concurrency:
jobs: jobs:
playwright-tests: playwright-tests:
timeout-minutes: 60 timeout-minutes: 120
runs-on: self-hosted environment: fork-pr-approval
runs-on: [self-hosted, nashua]
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
node-version: [20] node-version: [24.15.0]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: oven-sh/setup-bun@v2
with: with:
bun-version: 1.2.23 persist-credentials: false
- uses: actions/setup-node@v4 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- name: Install Yarn # No `cache: pnpm`: this is a self-hosted runner with a persistent
run: npm install -g yarn@1.22.22 # pnpm store on local disk. The Actions cache doesn't preserve the
# pnpm self-install's links faithfully, dropping pnpm/dist/worker.js
# and breaking `pnpm install` with MODULE_NOT_FOUND.
# Install pnpm via Corepack instead of pnpm/action-setup. The action's
# self-installer is a JS action run under the runner's bundled
# externals/node{version} and derives the `npm` path from that runtime — which
# on this self-hosted runner is corrupted (Cannot find module
# '../lib/cli.js'), so it dies regardless of `standalone:true`. Corepack
# ships inside the Node that setup-node just installed, reads the pinned
# `packageManager` (pnpm@11.5.2) from package.json, and fetches pnpm via
# Node's own https — it never invokes the npm CLI.
- name: Enable Corepack (pnpm)
shell: bash
run: |
corepack enable
corepack prepare --activate
# ── CS3D integration: detect label and ref type ──────────────────────
- name: Check for CS3D integration label
id: cs3d-check
run: bash .scripts/ci/cs3d-check-integration.sh
env:
GH_TOKEN: ${{ github.token }}
EVENT_NAME: ${{ github.event_name }}
CS3D_REF_INPUT: ${{ github.event.inputs.cs3d_ref || '4.19+' }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
- name: Detect CS3D ref type
id: cs3d-ref
if: steps.cs3d-check.outputs.enabled == 'true'
run: |
REF="${CS3D_REF}"
if [[ "$REF" =~ ^[0-9]+\.[0-9x]+\+?(\.[0-9x]+)?(-[a-zA-Z0-9._]+)?$ ]]; then
echo "type=version" >> "$GITHUB_OUTPUT"
RESOLVED=$(node .scripts/cs3d-resolve-version.mjs "$REF")
echo "version=$RESOLVED" >> "$GITHUB_OUTPUT"
echo "::notice::CS3D version: $REF -> $RESOLVED"
else
echo "type=branch" >> "$GITHUB_OUTPUT"
echo "::notice::CS3D branch: $REF"
fi
env:
CS3D_REF: ${{ steps.cs3d-check.outputs.cs3d_ref }}
# ── CS3D branch path: clone and build before OHIF install ───────────
- name: Clone CS3D
if: steps.cs3d-check.outputs.enabled == 'true' && steps.cs3d-ref.outputs.type == 'branch'
run: |
REF="${CS3D_REF}"
if [[ "$REF" == *:* ]]; then
REPO="https://github.com/${REF%%:*}/cornerstone3D.git"
BRANCH="${REF#*:}"
else
REPO="https://github.com/cornerstonejs/cornerstone3D.git"
BRANCH="$REF"
fi
echo "::notice::Cloning CS3D from $REPO branch $BRANCH"
git clone --depth 1 --branch "$BRANCH" "$REPO" libs/@cornerstonejs
env:
CS3D_REF: ${{ steps.cs3d-check.outputs.cs3d_ref }}
- name: Install & Build CS3D
if: steps.cs3d-check.outputs.enabled == 'true' && steps.cs3d-ref.outputs.type == 'branch'
working-directory: libs/@cornerstonejs
run: pnpm install --frozen-lockfile && pnpm run build:esm
# ── Common: install OHIF dependencies ───────────────────────────────
- name: Install dependencies - name: Install dependencies
run: bun install --frozen-lockfile run: pnpm install --frozen-lockfile
# ── CS3D branch path: link packages after OHIF install ──────────────
- name: Link CS3D packages
if: steps.cs3d-check.outputs.enabled == 'true' && steps.cs3d-ref.outputs.type == 'branch'
working-directory: libs/@cornerstonejs
run: node scripts/link-ohif-cornerstone-node-modules.mjs "$GITHUB_WORKSPACE"
# ── CS3D version path: update versions after OHIF install ───────────
- name: Set CS3D version
if: steps.cs3d-check.outputs.enabled == 'true' && steps.cs3d-ref.outputs.type == 'version'
run: |
node .scripts/cs3d-set-version.mjs "${CS3D_VERSION}"
pnpm install --no-frozen-lockfile
env:
CS3D_VERSION: ${{ steps.cs3d-ref.outputs.version }}
# ── Common: run tests ───────────────────────────────────────────────
- name: Install Playwright browsers - name: Install Playwright browsers
run: npx playwright install # Only chromium is used (see playwright.config.ts and tests/globalSetup.ts);
# firefox/webkit projects are commented out. Scoping the install to chromium
# avoids downloading + dep-validating browsers we never launch (the WebKit
# validation is what surfaces the "missing libwoff1/libflite1/..." error on
# hosts without those libs).
run: npx playwright install chromium
- name: Run Playwright tests - name: Run Playwright tests
run: | run: |
export NODE_OPTIONS="--max_old_space_size=10192" export NODE_OPTIONS="--max_old_space_size=10192"
bun run test:e2e:coverage bash .scripts/ci/with-nashua-lock.sh pnpm run test:e2e:coverage
# ── Common: collect test results and coverage ───────────────────────
- name: Create directory of test results - name: Create directory of test results
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
run: | run: |
mkdir -p packaged-test-results mkdir -p packaged-test-results
cp -r ./tests/test-results packaged-test-results/ || true cp -r ./tests/test-results packaged-test-results/ || true
cp ./tests/playwright-report.json packaged-test-results/ || true cp -r ./tests/playwright-report packaged-test-results/ || true
- name: Upload directory of test results artifact - name: Upload directory of test results artifact
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: playwright-results name: playwright-results
path: packaged-test-results/ path: packaged-test-results/
retention-days: 5 retention-days: 5
- name: create the coverage report - name: create the coverage report
run: | run: |
bun nyc report --reporter=lcov --reporter=text pnpm exec nyc report --reporter=lcov --reporter=text
- name: Upload the coverage report to GitHub Actions Artifacts - name: Upload the coverage report to GitHub Actions Artifacts
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: coverage-report-pr name: coverage-report-pr
path: coverage path: coverage
retention-days: 3 retention-days: 3
# ── CS3D: build and deploy preview to Netlify ───────────────────────
- name: Log build context (OHIF/CS3D branch and version for build diagnosis)
if: steps.cs3d-check.outputs.enabled == 'true'
run: |
if [[ "$CS3D_REF_TYPE" == "branch" ]]; then
echo "::notice::Build type: ohif-downstream | OHIF: ${{ github.repository }}@${{ github.ref }} (${{ github.sha }}) | CS3D: branch ${{ steps.cs3d-check.outputs.cs3d_ref }}"
else
echo "::notice::Build type: ohif-upstream | OHIF: ${{ github.repository }}@${{ github.ref }} (${{ github.sha }}) | CS3D: version ${{ steps.cs3d-ref.outputs.version }}"
fi
node .scripts/log-build-context.mjs
env:
BUILD_TYPE:
${{ steps.cs3d-ref.outputs.type == 'branch' && 'ohif-downstream' || 'ohif-upstream' }}
CS3D_REF_TYPE: ${{ steps.cs3d-ref.outputs.type }}
- name: Build OHIF viewer (CS3D preview)
if: steps.cs3d-check.outputs.enabled == 'true'
run: pnpm run build:ci
- name: Deploy CS3D preview to Netlify
if: steps.cs3d-check.outputs.enabled == 'true'
run: |
RESULT=$(npx netlify-cli deploy --dir=platform/app/dist --alias="cs3d-pr-${PR_NUM}" --json --filter=@ohif/app) || {
echo "::error::Netlify deploy command failed"
exit 1
}
URL=$(echo "$RESULT" | jq -r '.deploy_url')
if [[ -z "$URL" || "$URL" == "null" ]]; then
echo "::error::Netlify deploy did not return a valid URL"
echo "$RESULT"
exit 1
fi
echo "::notice::CS3D preview deployed: $URL"
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
PR_NUM: ${{ github.event.pull_request.number || 'manual' }}
# ── CS3D: log results ───────────────────────────────────────────────
- name: Log CS3D build used
if: steps.cs3d-check.outputs.enabled == 'true'
run: |
if [[ "$CS3D_REF_TYPE" == "branch" ]]; then
echo "::notice::CS3D integration PASSED with branch ${CS3D_REF} (linked from libs/@cornerstonejs)"
else
echo "::notice::CS3D integration PASSED with @cornerstonejs/*@${CS3D_VERSION}"
fi
env:
CS3D_REF_TYPE: ${{ steps.cs3d-ref.outputs.type }}
CS3D_REF: ${{ steps.cs3d-check.outputs.cs3d_ref }}
CS3D_VERSION: ${{ steps.cs3d-ref.outputs.version }}
# ── Separate job: block merge when using a CS3D branch ─────────────
cs3d-branch-merge-guard:
name: 'CS3D Branch Merge Guard'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Check for CS3D branch usage
run: bash .scripts/ci/cs3d-branch-merge-guard.sh
env:
GH_TOKEN: ${{ github.token }}
EVENT_NAME: ${{ github.event_name }}
CS3D_REF_INPUT: ${{ github.event.inputs.cs3d_ref || '4.19+' }}
REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}

11
.gitignore vendored
View File

@ -1,6 +1,7 @@
# Packages # Packages
node_modules node_modules
.cursor/ .cursor/
.claude/
.nyc_output/ .nyc_output/
# Output # Output
build build
@ -44,6 +45,7 @@ videos/
# autogenerated files # autogenerated files
platform/app/src/pluginImports.js platform/app/src/pluginImports.js
CLAUDE.md
/Viewers.iml /Viewers.iml
platform/app/.recipes/Nginx-Dcm4Chee/logs/* platform/app/.recipes/Nginx-Dcm4Chee/logs/*
platform/app/.recipes/OpenResty-Orthanc/logs/* platform/app/.recipes/OpenResty-Orthanc/logs/*
@ -59,7 +61,14 @@ tests/playwright-report/
/blob-report/ /blob-report/
/playwright/.cache/ /playwright/.cache/
**/.claude/settings.local.json # cornerstone3D local linking
libs/
# Backup files # Backup files
*~ *~
# cornerstone3D local linking
libs/
link-cs3d.js
unlink-cs3d.js
auth.json

View File

@ -6,27 +6,21 @@ cd "$(dirname "$0")"
cd .. # Up to project root cd .. # Up to project root
# Helpful to verify which versions we're using # Helpful to verify which versions we're using
echo 'My yarn version is... ' echo 'My pnpm version is... '
yarn -v pnpm -v
node -v node -v
# Build && Move PWA Output # Build && Move PWA Output
yarn run build:ci pnpm run build:ci
mkdir -p ./.netlify/www/pwa mkdir -p ./.netlify/www/pwa
mv platform/app/dist/* .netlify/www/pwa -v mv platform/app/dist/* .netlify/www/pwa -v
echo 'Web application built and copied' echo 'Web application built and copied'
# Build && Move Docusaurus Output (for the docs themselves) # Build && Move Docusaurus Output (for the docs themselves)
cd platform/docs pnpm --filter ohif-docs run build
yarn install --frozen-lockfile
yarn run build
cd ../..
mkdir -p ./.netlify/www/docs mkdir -p ./.netlify/www/docs
mv platform/docs/build/* .netlify/www/docs -v mv platform/docs/build/* .netlify/www/docs -v
echo 'Docs built (docusaurus) and copied' echo 'Docs built (docusaurus) and copied'
# Cache all of the node_module dependencies in
# extensions, modules, and platform packages
yarn run lerna:cache
echo 'Nothing left to see here. Go home, folks.' echo 'Nothing left to see here. Go home, folks.'

View File

@ -2,9 +2,8 @@
"name": "root", "name": "root",
"private": true, "private": true,
"engines": { "engines": {
"node": ">=14", "node": ">=24",
"npm": ">=6", "pnpm": ">=11"
"yarn": ">=1.16.0"
}, },
"scripts": { "scripts": {
"deploy": "netlify deploy --prod --dir ./../platform/app/dist" "deploy": "netlify deploy --prod --dir ./../platform/app/dist"

View File

@ -1 +1 @@
20.9.0 24.15.0

5
.npmrc Normal file
View File

@ -0,0 +1,5 @@
node-linker=hoisted
strict-peer-dependencies=false
link-workspace-packages=true
prefer-workspace-packages=true
manage-package-manager-versions=false

View File

@ -0,0 +1,42 @@
#!/usr/bin/env bash
# CS3D branch merge guard: blocks merge when tests ran against a CS3D branch (not a version).
# Exits 0 when merge is allowed or guard is skipped; exits 1 when merge must be blocked.
#
# Required env: GH_TOKEN, EVENT_NAME, REPO, PR_NUMBER
# Optional env: CS3D_REF_INPUT (for workflow_dispatch, default 4.19+)
set -e
if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
echo "::notice::workflow_dispatch — no merge to block, skipping guard."
exit 0
elif [[ "$EVENT_NAME" == "pull_request" ]]; then
LABELS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/labels" --jq '.[].name')
if echo "$LABELS" | grep -q "ohif-integration"; then
ENABLED=true
CS3D_REF=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.body' \
| sed -n 's/^[[:space:]]*CS3D_REF:[[:space:]]*\([^[:space:]]*\).*/\1/p' | head -1)
if [[ -z "$CS3D_REF" ]]; then
CS3D_REF="4.19+"
fi
else
ENABLED=false
fi
else
ENABLED=false
fi
if [[ "$ENABLED" != "true" ]]; then
echo "::notice::No ohif-integration label — skipping merge guard."
exit 0
fi
# Check if the ref is a branch (not a version)
if [[ "$CS3D_REF" =~ ^[0-9]+\.[0-9x]+\+?(\.[0-9x]+)?(-[a-zA-Z0-9._]+)?$ ]]; then
echo "::notice::CS3D ref '$CS3D_REF' is a version — merge allowed."
exit 0
fi
echo "::error::Tests ran against CS3D branch '${CS3D_REF}' — this build cannot be merged."
echo "::error::Re-run with a published CS3D version (e.g. 4.19+) before merging."
exit 1

View File

@ -0,0 +1,29 @@
#!/usr/bin/env bash
# CS3D integration check: detects ohif-integration label and parses CS3D_REF.
# Writes to GITHUB_OUTPUT: enabled (true|false), cs3d_ref (when enabled).
#
# Required env: GH_TOKEN, EVENT_NAME, REPO, PR_NUMBER, GITHUB_OUTPUT
# Optional env: CS3D_REF_INPUT (for workflow_dispatch, default 4.19+)
set -e
if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
echo "cs3d_ref=${CS3D_REF_INPUT:-4.19+}" >> "$GITHUB_OUTPUT"
elif [[ "$EVENT_NAME" == "pull_request" ]]; then
LABELS=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/labels" --jq '.[].name')
if echo "$LABELS" | grep -q "ohif-integration"; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
REF=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.body' \
| sed -n 's/^[[:space:]]*CS3D_REF:[[:space:]]*\([^[:space:]]*\).*/\1/p' | head -1)
if [[ -z "$REF" ]]; then
REF="4.19+"
fi
echo "cs3d_ref=${REF}" >> "$GITHUB_OUTPUT"
echo "::notice::CS3D ref from PR body: ${REF}"
else
echo "enabled=false" >> "$GITHUB_OUTPUT"
fi
else
echo "enabled=false" >> "$GITHUB_OUTPUT"
fi

View File

@ -0,0 +1,54 @@
#!/usr/bin/env bash
#
# Run a command while holding the nashua Playwright mutex.
#
# Only one Playwright run may execute at a time on the shared nashua self-hosted
# runner — across THIS repo (OHIF) AND the cornerstone3D repo. GitHub's
# `concurrency:` is scoped per-repository and cannot coordinate across the two
# orgs, so the mutex lives on the box's filesystem.
#
# This is OHIF's own copy (the script can't be shared across separate repos). It
# MUST use the SAME lock path as cornerstone3D's copy (NASHUA_LOCK_FILE default
# below) — if the paths differ, the mutex silently does nothing.
#
# flock holds the lock via fd 9 and it releases automatically when the wrapped
# command exits — including on job cancel / timeout / SIGKILL — so there are no
# stale locks to clean up.
#
# Usage: .scripts/ci/with-nashua-lock.sh <command> [args...]
# Strict mode: -e abort on any unhandled command failure, -u treat use of an
# unset variable as an error, -o pipefail make a pipeline fail if ANY stage
# fails (not just the last). Catches mistakes early instead of pressing on.
set -euo pipefail
LOCK="${NASHUA_LOCK_FILE:-/var/tmp/nashua-playwright.lock}" # MUST match cornerstone3D's copy
LOCK_WAIT="${NASHUA_LOCK_WAIT:-5400}" # max seconds to wait
# Guard: a command to wrap is required. With no arguments there is nothing to
# run, so print usage and exit instead of silently grabbing and releasing the
# lock for no reason (which would mask a mis-wired workflow step).
if [ "$#" -eq 0 ]; then
echo "usage: $0 <command> [args...]" >&2
exit 2
fi
# Open the lock file on fd 9 (read-write, create if missing, never truncate).
exec 9<>"$LOCK" || { echo "::error::cannot open lock file $LOCK"; exit 1; }
# Try instantly; if busy, report who holds it, then block up to LOCK_WAIT.
if ! flock -n 9; then
echo "nashua Playwright runner busy — held by: $(cat "${LOCK}.info" 2>/dev/null || echo unknown). Waiting up to ${LOCK_WAIT}s…"
if ! flock -w "$LOCK_WAIT" 9; then
echo "::error::Timed out after ${LOCK_WAIT}s waiting for the nashua Playwright lock"
exit 1
fi
fi
# Record the current holder for other jobs' "held by" message (best-effort).
echo "${GITHUB_REPOSITORY:-local}#${GITHUB_RUN_ID:-0} @ $(date -u +%FT%TZ 2>/dev/null || true)" > "${LOCK}.info" 2>/dev/null || true
echo "✅ Acquired nashua Playwright lock ($LOCK); running: $*"
# Replace the shell with the command. fd 9 is inherited (not close-on-exec), so
# the lock is held for the whole command and released when it exits.
exec "$@"

55
.scripts/cs3d-check.mjs Normal file
View File

@ -0,0 +1,55 @@
import fs from 'node:fs';
import path from 'node:path';
import { execSync } from 'node:child_process';
const workflowPath = path.resolve(
process.cwd(),
'libs',
'@cornerstonejs',
'.github',
'workflows',
'ohif-downstream.yml'
);
if (!fs.existsSync(workflowPath)) {
console.error(`[cs3d:check] Workflow file not found: ${workflowPath}`);
process.exit(1);
}
const workflowText = fs.readFileSync(workflowPath, 'utf8');
const ohifRefMatch = workflowText.match(/^\s*OHIF_REF:\s*["']?([^"'\r\n]+)["']?\s*$/m);
if (!ohifRefMatch) {
console.error('[cs3d:check] Could not find OHIF_REF in ohif-downstream workflow.');
process.exit(1);
}
const expectedBranch = ohifRefMatch[1].trim();
let currentBranch = '';
try {
currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
cwd: process.cwd(),
encoding: 'utf8',
}).trim();
} catch (error) {
console.error('[cs3d:check] Failed to determine current git branch.');
process.exit(1);
}
if (currentBranch !== expectedBranch) {
console.error(
`[cs3d:check] Branch mismatch: current='${currentBranch}', expected='${expectedBranch}' from ${path.relative(
process.cwd(),
workflowPath
)}`
);
process.exit(1);
}
console.log(
`[cs3d:check] OK: current branch '${currentBranch}' matches OHIF_REF in ${path.relative(
process.cwd(),
workflowPath
)}`
);

View File

@ -0,0 +1,63 @@
#!/usr/bin/env node
/**
* Resolves a version pattern to a concrete npm version of @cornerstonejs/core.
*
* Patterns:
* 4.18.2 -> 4.18.2 (exact, returned as-is)
* 4.18.2-beta.3 -> 4.18.2-beta.3 (exact prerelease)
* 4.x -> latest 4.* from npm
* 4.17.x -> latest 4.17.* from npm
* 4.19+ -> latest >=4.19.0 <5.0.0-0 from npm (4.19 and later, same major)
*
* Prints the resolved version to stdout.
*/
import { execSync } from 'child_process';
const pattern = process.argv[2];
if (!pattern) {
console.error('Usage: cs3d-resolve-version.mjs <pattern>');
console.error(' e.g. 4.x, 4.17.x, 4.19+, 4.18.2, 4.18.2-beta.3');
process.exit(1);
}
// Exact version (no wildcard or range) — pass through unchanged
if (!pattern.includes('x') && !pattern.endsWith('+')) {
console.log(pattern);
process.exit(0);
}
// Convert "M.m+" to npm semver range ">=M.m.0 <(M+1).0.0-0"
let npmRange = pattern;
const plusMatch = pattern.match(/^(\d+)\.(\d+)\+$/);
if (plusMatch) {
const major = Number(plusMatch[1]);
const minor = Number(plusMatch[2]);
npmRange = `>=${major}.${minor}.0 <${major + 1}.0.0-0`;
}
try {
// Let npm resolve the range: @package@<range> → concrete version
const raw = execSync(`npm view @cornerstonejs/core@"${npmRange}" version --json`, {
encoding: 'utf8',
timeout: 30_000,
});
const resolved = JSON.parse(raw);
if (!resolved) {
console.error(`npm returned no version when resolving @cornerstonejs/core@${npmRange}`);
process.exit(1);
}
// npm may return an array of versions for ranges; pick the latest (last) one
const version = Array.isArray(resolved) ? resolved[resolved.length - 1] : resolved;
console.log(version);
} catch (err) {
const message =
(err && (err.stderr?.toString() || err.message || String(err))) ||
`Unknown error resolving @cornerstonejs/core@${pattern}`;
console.error(`Failed to resolve @cornerstonejs/core version for pattern "${pattern}":`);
console.error(message);
process.exit(1);
}

View File

@ -0,0 +1,131 @@
#!/usr/bin/env node
/**
* Updates all @cornerstonejs/* package versions across the OHIF workspace.
*
* Usage: node .scripts/cs3d-set-version.mjs <version>
*
* Only updates the 8 main CS3D packages (not codec packages):
* adapters, ai, core, dicom-image-loader, labelmap-interpolation,
* nifti-volume-loader, polymorphic-segmentation, tools
*/
import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
import { resolve, dirname, join } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = resolve(__dirname, '..');
const version = process.argv[2];
if (!version) {
console.error('Usage: cs3d-set-version.mjs <version>');
console.error(' e.g. 4.18.2, 4.19.0-beta.1');
process.exit(1);
}
// The 8 CS3D packages that are built from source (not codecs)
const CS3D_PACKAGES = [
'@cornerstonejs/adapters',
'@cornerstonejs/ai',
'@cornerstonejs/core',
'@cornerstonejs/dicom-image-loader',
'@cornerstonejs/labelmap-interpolation',
'@cornerstonejs/nifti-volume-loader',
'@cornerstonejs/polymorphic-segmentation',
'@cornerstonejs/tools',
];
// Read root package.json to get workspace globs
const rootPkgPath = resolve(rootDir, 'package.json');
const rootPkg = JSON.parse(readFileSync(rootPkgPath, 'utf8'));
const workspaceGlobs = rootPkg.workspaces?.packages || rootPkg.workspaces || [];
// Collect all package.json paths from workspace globs
function findWorkspacePackageJsons() {
const paths = [rootPkgPath]; // include root
for (const pattern of workspaceGlobs) {
const parts = pattern.split('/');
let searchDir = rootDir;
let hasWildcard = false;
for (const part of parts) {
if (part === '*') {
hasWildcard = true;
break;
}
searchDir = join(searchDir, part);
}
if (hasWildcard) {
try {
const entries = readdirSync(searchDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const pkgJson = join(searchDir, entry.name, 'package.json');
if (existsSync(pkgJson)) {
paths.push(pkgJson);
}
}
}
} catch {
// directory doesn't exist, skip
}
} else {
const pkgJson = join(rootDir, pattern, 'package.json');
if (existsSync(pkgJson)) {
paths.push(pkgJson);
}
}
}
return paths;
}
// Update a dependencies object, returning count of changes
function updateDeps(deps, targetVersion) {
let count = 0;
if (!deps) return count;
for (const pkg of CS3D_PACKAGES) {
if (pkg in deps && deps[pkg] !== targetVersion) {
deps[pkg] = targetVersion;
count++;
}
}
return count;
}
const pkgPaths = findWorkspacePackageJsons();
let totalChanges = 0;
for (const pkgPath of pkgPaths) {
const content = readFileSync(pkgPath, 'utf8');
const pkg = JSON.parse(content);
let changes = 0;
changes += updateDeps(pkg.dependencies, version);
changes += updateDeps(pkg.devDependencies, version);
changes += updateDeps(pkg.peerDependencies, version);
changes += updateDeps(pkg.resolutions, version);
if (changes > 0) {
// Preserve original formatting (detect indent — restrict to spaces/tabs so
// we don't accidentally capture a CRLF newline as part of the indent string)
const indent = content.match(/^([ \t]+)/m)?.[1] || ' ';
writeFileSync(pkgPath, JSON.stringify(pkg, null, indent) + '\n');
const rel = pkgPath.replace(rootDir + '/', '').replace(rootDir + '\\', '');
console.log(` Updated ${rel} (${changes} packages)`);
totalChanges += changes;
}
}
console.log(
`\nDone: ${totalChanges} version(s) updated to ${version} across ${pkgPaths.length} package files.`
);
console.log(
'This step changes package.json; the following install must not use a frozen Bun lockfile ' +
'(OHIF+CS3D combined “version” CI does: `bun install --config=./bunfig.update-lockfile.toml`). ' +
'Other installs stay frozen. Locally after this script, use that bun command and/or ' +
'`bun run install:update-lockfile` when you intend to commit lockfile updates.\n'
);

View File

@ -6,9 +6,9 @@ PROJECT=$1
if [ -z "$PROJECT" ] if [ -z "$PROJECT" ]
then then
# Default # Default
npx lerna run dev:viewer pnpm --filter @ohif/app run dev:viewer
else else
eval "npx lerna run dev:$PROJECT" pnpm --filter @ohif/app run "dev:$PROJECT"
fi fi
read -p 'Press [Enter] key to continue...' read -p 'Press [Enter] key to continue...'

View File

@ -0,0 +1,108 @@
#!/usr/bin/env node
/**
* Logs build context (OHIF branch/version, CS3D source) for diagnosing build issues on GitHub.
* Run from repo root. Used by version.mjs and can be invoked from CI workflows.
*/
import { execa } from 'execa';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '..');
function log(msg) {
console.log(`[build-context] ${msg}`);
}
async function detectCs3dSource() {
const libsCs3d = path.join(REPO_ROOT, 'libs/@cornerstonejs');
const coreInNodeModules = path.join(REPO_ROOT, 'node_modules/@cornerstonejs/core');
try {
const libsExists = await fs.access(libsCs3d).then(() => true).catch(() => false);
if (!libsExists) {
return { source: 'npm', detail: 'published @cornerstonejs packages (no libs/@cornerstonejs)' };
}
const coreStat = await fs.lstat(coreInNodeModules).catch(() => null);
const isSymlink = coreStat?.isSymbolicLink();
if (isSymlink) {
const target = await fs.readlink(coreInNodeModules);
return { source: 'integrated', detail: `linked from libs/@cornerstonejs (→ ${target})` };
}
return { source: 'npm', detail: 'published @cornerstonejs packages (libs exists but not linked)' };
} catch {
return { source: 'unknown', detail: 'could not detect' };
}
}
async function getCs3dVersion() {
try {
const corePkg = path.join(REPO_ROOT, 'node_modules/@cornerstonejs/core/package.json');
const pkg = JSON.parse(await fs.readFile(corePkg, 'utf-8'));
return pkg.version || 'unknown';
} catch {
return 'unknown';
}
}
async function getCs3dBranchFromLibs() {
try {
const libsCs3d = path.join(REPO_ROOT, 'libs/@cornerstonejs');
const { stdout } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
cwd: libsCs3d,
});
return stdout;
} catch {
return null;
}
}
async function run() {
const inCI = process.env.GITHUB_ACTIONS === 'true';
const buildType = inCI ? (process.env.BUILD_TYPE || 'ohif-upstream') : 'local';
log('═══════════════════════════════════════════════════════════════');
log('Build context (for diagnosing GitHub build issues)');
log('═══════════════════════════════════════════════════════════════');
if (inCI) {
log(`Build type: ${process.env.BUILD_TYPE || 'ohif-upstream'}`);
log(`GitHub repo: ${process.env.GITHUB_REPOSITORY || 'unknown'}`);
log(`GitHub ref: ${process.env.GITHUB_REF || 'unknown'}`);
log(`GitHub SHA: ${process.env.GITHUB_SHA || 'unknown'}`);
log(`Workflow: ${process.env.GITHUB_WORKFLOW || 'unknown'}`);
}
try {
const { stdout: branch } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
cwd: REPO_ROOT,
});
const { stdout: sha } = await execa('git', ['rev-parse', '--short', 'HEAD'], {
cwd: REPO_ROOT,
});
log(`OHIF branch: ${branch} (${sha})`);
} catch {
log('OHIF branch: (not a git repo or error)');
}
const cs3d = await detectCs3dSource();
log(`CS3D source: ${cs3d.source}${cs3d.detail}`);
if (cs3d.source === 'integrated') {
const branch = await getCs3dBranchFromLibs();
if (branch) log(`CS3D branch (libs/@cornerstonejs): ${branch}`);
} else {
const ver = await getCs3dVersion();
log(`CS3D version (@cornerstonejs/core): ${ver}`);
}
log('═══════════════════════════════════════════════════════════════');
}
run().catch((err) => {
console.error('[build-context] Error:', err.message);
process.exitCode = 1;
});

51
.webpack/resolveConfig.js Normal file
View File

@ -0,0 +1,51 @@
const path = require('path');
// Shared module-resolution rules (alias + module search paths) used by BOTH
// build pipelines so they cannot drift apart:
// - the webpack/rspack build: webpack.base.js -> webpack.pwa.js, plus every
// per-package webpack.prod.js / webpack.dev.js that merges webpack.base.js
// - the rsbuild build: rsbuild.config.ts (dev:fast)
//
// Plugin aliases (writePluginImportsFile.getPluginResolveAliases) are NOT
// included here: they depend on pluginConfig.json and are merged in separately
// by each config.
//
// All paths are anchored to this file's location (the repo-root `.webpack/`
// directory), so the values are identical regardless of which package's build
// is doing the resolving.
const alias = {
// Some extensions import app-level utilities (e.g. history,
// preserveQueryParameters) from '@ohif/app'. pnpm's isolated layout does not
// expose the top-level app package to those extensions, and adding it as a
// workspace dependency would create an app<->default cycle, so we resolve the
// bare specifier to the app source here ($ = exact match).
'@ohif/app$': path.resolve(__dirname, '../platform/app/src/index.js'),
'@': path.resolve(__dirname, '../platform/app/src'),
'@components': path.resolve(__dirname, '../platform/app/src/components'),
'@hooks': path.resolve(__dirname, '../platform/app/src/hooks'),
'@routes': path.resolve(__dirname, '../platform/app/src/routes'),
'@state': path.resolve(__dirname, '../platform/app/src/state'),
};
// Directories to search when resolving modules. The leading bare 'node_modules'
// preserves the default importer-relative walk-up, which pnpm's isolated layout
// requires so that transitive deps (e.g. react-remove-scroll -> tslib 2.x)
// resolve to the sibling copy inside .pnpm/<pkg>/node_modules rather than a
// hoisted older version.
const moduleSearchPaths = [
'node_modules',
path.resolve(__dirname, '../node_modules'),
path.resolve(__dirname, '../../../node_modules'),
path.resolve(__dirname, '../platform/app/node_modules'),
path.resolve(__dirname, '../platform/ui/node_modules'),
];
// Build the resolve.modules array for a build. `srcDir` is the building
// package's source root; it is appended last (matching the previous inline
// behavior in webpack.base.js). Pass nothing to get just the shared paths.
function getModules(srcDir) {
return srcDir ? [...moduleSearchPaths, srcDir] : [...moduleSearchPaths];
}
module.exports = { alias, moduleSearchPaths, getModules };

View File

@ -2,14 +2,14 @@ const autoprefixer = require('autoprefixer');
const path = require('path'); const path = require('path');
const tailwindcss = require('tailwindcss'); const tailwindcss = require('tailwindcss');
const tailwindConfigPath = path.resolve('../../platform/app/tailwind.config.js'); const tailwindConfigPath = path.resolve('../../platform/app/tailwind.config.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const rspack = require('@rspack/core');
const devMode = process.env.NODE_ENV !== 'production'; const devMode = process.env.NODE_ENV !== 'production';
const cssToJavaScript = { const cssToJavaScript = {
test: /\.css$/, test: /\.css$/,
use: [ use: [
//'style-loader', //'style-loader',
devMode ? 'style-loader' : MiniCssExtractPlugin.loader, devMode ? 'style-loader' : rspack.CssExtractRspackPlugin.loader,
{ loader: 'css-loader', options: { importLoaders: 1 } }, { loader: 'css-loader', options: { importLoaders: 1 } },
{ {
loader: 'postcss-loader', loader: 'postcss-loader',

View File

@ -4,6 +4,8 @@ function transpileJavaScript(mode) {
const exclude = const exclude =
mode === 'production' mode === 'production'
? excludeNodeModulesExcept([ ? excludeNodeModulesExcept([
// Workspace packages (needed for pnpm shamefully-hoist where they resolve through node_modules)
'@ohif',
// 'dicomweb-client', // 'dicomweb-client',
// https://github.com/react-dnd/react-dnd/blob/master/babel.config.js // https://github.com/react-dnd/react-dnd/blob/master/babel.config.js
'react-dnd', 'react-dnd',

View File

@ -4,11 +4,10 @@ const dotenv = require('dotenv');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const webpack = require('webpack'); const webpack = require('@rspack/core');
// ~~ PLUGINS // ~~ PLUGINS
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; // const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const TerserJSPlugin = require('terser-webpack-plugin');
// ~~ PackageJSON // ~~ PackageJSON
// const vtkRules = require('vtk.js/Utilities/config/dependency.js').webpack.core // const vtkRules = require('vtk.js/Utilities/config/dependency.js').webpack.core
@ -18,9 +17,15 @@ const TerserJSPlugin = require('terser-webpack-plugin');
const loadWebWorkersRule = require('./rules/loadWebWorkers.js'); const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
const transpileJavaScriptRule = require('./rules/transpileJavaScript.js'); const transpileJavaScriptRule = require('./rules/transpileJavaScript.js');
const cssToJavaScript = require('./rules/cssToJavaScript.js'); const cssToJavaScript = require('./rules/cssToJavaScript.js');
// Module-resolution rules shared with the rsbuild build (see rsbuild.config.ts).
const resolveConfig = require('./resolveConfig');
// Only uncomment for old v2 stylus // Only uncomment for old v2 stylus
// const stylusToJavaScript = require('./rules/stylusToJavaScript.js'); // const stylusToJavaScript = require('./rules/stylusToJavaScript.js');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); let ReactRefreshWebpackPlugin;
try {
const mod = require('@rspack/plugin-react-refresh');
ReactRefreshWebpackPlugin = mod.ReactRefreshRspackPlugin || mod.default || mod;
} catch { ReactRefreshWebpackPlugin = null; }
// ~~ ENV VARS // ~~ ENV VARS
const NODE_ENV = process.env.NODE_ENV; const NODE_ENV = process.env.NODE_ENV;
@ -66,6 +71,12 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
const config = { const config = {
mode: isProdBuild ? 'production' : 'development', mode: isProdBuild ? 'production' : 'development',
devtool: isProdBuild ? 'source-map' : 'cheap-module-source-map', devtool: isProdBuild ? 'source-map' : 'cheap-module-source-map',
// `rspack serve` (@rspack/cli) auto-enables lazyCompilation for web-only
// apps unless the config defines it explicitly. The on-demand proxy chunks
// it produces fail to load in the headless cypress/electron e2e run
// (ChunkLoadError on cornerstone vendor chunks), so disable it here to match
// the rsbuild build (see rsbuild.config.ts `dev.lazyCompilation: false`).
lazyCompilation: false,
entry: ENTRY, entry: ENTRY,
optimization: { optimization: {
// splitChunks: { // splitChunks: {
@ -92,9 +103,7 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
children: false, children: false,
warnings: true, warnings: true,
}, },
cache: { cache: isProdBuild ? false : { type: 'filesystem' },
type: 'filesystem',
},
module: { module: {
noParse: [/(dicomicc)/], noParse: [/(dicomicc)/],
rules: [ rules: [
@ -168,9 +177,6 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
}, },
}, },
cssToJavaScript, cssToJavaScript,
// Note: Only uncomment the following if you are using the old style of stylus in v2
// Also you need to uncomment this platform/app/.webpack/rules/extractStyleChunks.js
// stylusToJavaScript,
{ {
test: /\.wasm/, test: /\.wasm/,
type: 'asset/resource', type: 'asset/resource',
@ -194,27 +200,16 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
}, },
resolve: { resolve: {
mainFields: ['module', 'browser', 'main'], mainFields: ['module', 'browser', 'main'],
// alias and modules are shared with the rsbuild build via ./resolveConfig
// so the two pipelines resolve identically.
alias: { alias: {
// Viewer project ...resolveConfig.alias,
'@': path.resolve(__dirname, '../platform/app/src'),
'@components': path.resolve(__dirname, '../platform/app/src/components'),
'@hooks': path.resolve(__dirname, '../platform/app/src/hooks'),
'@routes': path.resolve(__dirname, '../platform/app/src/routes'),
'@state': path.resolve(__dirname, '../platform/app/src/state'),
}, },
// Which directories to search when resolving modules modules: resolveConfig.getModules(SRC_DIR),
modules: [
// Modules specific to this package
path.resolve(__dirname, '../node_modules'),
// Hoisted Yarn Workspace Modules
path.resolve(__dirname, '../../../node_modules'),
path.resolve(__dirname, '../platform/app/node_modules'),
path.resolve(__dirname, '../platform/ui/node_modules'),
SRC_DIR,
],
// Attempt to resolve these extensions in order. // Attempt to resolve these extensions in order.
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx', '*'], extensions: ['.js', '.jsx', '.json', '.ts', '.tsx', '*'],
// symlinked resources are resolved to their real path, not their symlinked location // Workspace packages use relative imports between sibling packages.
// Resolve symlinks to keep those imports anchored at the real package paths.
symlinks: true, symlinks: true,
fallback: { fallback: {
fs: false, fs: false,
@ -223,24 +218,34 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
buffer: require.resolve('buffer'), buffer: require.resolve('buffer'),
}, },
}, },
node: {
// Leave __filename / __dirname references alone. The previous 'mock'
// value triggers an rspack warning whenever bundled deps reference
// __dirname (e.g. Emscripten-compiled cornerstone codecs). Those refs
// sit inside `if (ENVIRONMENT_IS_NODE)` branches that never execute in
// the browser, so leaving them un-substituted is harmless at runtime.
__filename: false,
__dirname: false,
},
plugins: [ plugins: [
new webpack.DefinePlugin(defineValues), new webpack.DefinePlugin(defineValues),
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'], Buffer: ['buffer', 'Buffer'],
}), }),
...(isProdBuild ? [] : [new ReactRefreshWebpackPlugin({ overlay: false })]), new webpack.IgnorePlugin({
resourceRegExp: /^(fs|path)$/,
contextRegExp: /@cornerstonejs[\\/]codec-/,
}),
...(isProdBuild || IS_COVERAGE || !ReactRefreshWebpackPlugin
? []
: [new ReactRefreshWebpackPlugin({ overlay: false })]),
// Uncomment to generate bundle analyzer // Uncomment to generate bundle analyzer
// new BundleAnalyzerPlugin(), // new BundleAnalyzerPlugin(),
], ],
}; };
if (isProdBuild) { if (isProdBuild) {
config.optimization.minimizer = [ config.optimization.minimizer = [new webpack.SwcJsMinimizerRspackPlugin()];
new TerserJSPlugin({
parallel: true,
terserOptions: {},
}),
];
} }
if (isQuickBuild) { if (isQuickBuild) {

205
AGENTS.md Normal file
View File

@ -0,0 +1,205 @@
# AGENTS.md
This file provides guidance to AI coding agents (Claude, Codex, and other LLM tools) when working with code in this repository.
## Project Overview
This is **OHIF** v3 (Open Health Imaging Foundation) - a medical imaging viewer. It's an extensible web imaging platform.
## Development Commands
### Main Development
```bash
# Start development server for all packages
yarn dev
```
### Building
```bash
# Build all packages for production
yarn build
# Build specific packages
cd platform/app && yarn build # Main viewer app
```
## Architecture Overview
### Monorepo Structure
- **`platform/`** - Core OHIF infrastructure
- `app/` - Main viewer application (`@ohif/viewer`)
- `core/` - Core services and utilities
- `ui-next/` - Modern UI component library
- **`extensions/`** - Modular functionality plugins
- **`modes/`** - Application workflow configurations
### Key Extension Architecture
**Extension System**: Each extension exports modules (viewports, tools, panels, commands) that the app dynamically loads. Extensions are self-contained with their own webpack builds.
**Core Extensions:**
- `cornerstone/` - Medical image rendering engine
- `cornerstone-dicom-pmp/` - DICOM PMP support
- `cornerstone-dicom-seg/` - DICOM Segmentation support
- `cornerstone-dicom-sr/` - DICOM SR support
- `dicom-pdf/` - DICOM PDF support
- `dicom-video/` - DICOM Video support
- `measurement-tracking/` - Measurement tracking support
- `default/` - Standard OHIF functionality
### Service-Oriented Design (PUB-SUB)
The app uses a Services Manager pattern with these core services:
- **Display Set Service**: Manages image series organization
- **Measurement Service**: Handles annotations and measurements
- **Hanging Protocol Service**: Controls image layout and display rules
- **UI Service**: Manages panels, modals, and notifications
- **Segmentation Service**: AI/ML powered image segmentation, loading segmentations, etc.
- **Viewport Grid Service**: Manages viewport layout and display rules
- **Viewport Display Set History Service**: Manages viewport display set history
- **Viewport Dialog Service**: Manages viewport dialogs
- **Notification Service**: Manages notifications
- **Modal Service**: Manages modals
- **Dialog Service**: Manages dialogs, more general not just viewport dialogs
- **Customization Service**: Manages customization of the app
- **Toolbar Service**: Manages the toolbar, viewport action corners, tool states
- **User Authentication Service**: Manages user authentication, but used only for injecting tokens in dicomweb requests in our context
- **Panel Service**: Manages side panels
- **Cornerstone Viewport Service**: Manages the cornerstone viewport, rendering engines, presentation states, more tightly coupled to cornerstone than the other services
- **Tool Group Service**: Manages tool groups, creating and managing tool groups, etc.
- **Sync Group Service**: Manages sync groups, syncing zooming, panning, scrolling, etc.
- **Cornerstone Cache Service**: Manages the cornerstone cache, caching images, etc.
Most of the services utilize a pub sub architecture and extend the pub sub service interace at `pubSubServiceInterface.ts`
### Commands Manager
The Commands Manager tracks named commands (or functions) that are scoped to
a context. When we attempt to run a command with a given name, we look for it
in our active contexts, in the order specified.
If found, we run the command, passing in any application
or call specific data specified in the command's definition.
You can call `commandsManager.runCommand` to run a command.
### Extension Manager
Aggregates and exposes extension modules throughout the OHIF application, manages data sources, and provides a centralized registry for accessing extension functionality.
### Build System
**Yarn Workspaces**: Optimized monorepo builds with dependency caching
**Webpack 5**: Module federation for dynamic extension loading
**Plugin Import System**: Extensions auto-register via `writePluginImportsFile.js`
### Key Technologies
- **React 18 + TypeScript**: UI framework
- **Cornerstone.js**: Medical image rendering
- **DICOM**: Medical imaging standard support
- **ONNX Runtime**: AI model inference (SAM segmentation models)
- **Zustand**: State management
- **TailwindCSS**: Styling system
## Development Patterns
### Adding New Tools
1. Create tool class in `extensions/cornerstone/src/tools/`
2. Register in tool module's `toolNames.ts`
3. Add to toolbar via `getToolbarModule.tsx`
4. Add measurement mapping if needed in `measurementServiceMappings/`
### Creating Extensions
Extensions must export:
- `id.js` - Unique extension identifier
- `index.tsx` - Extension registration
- Module functions (`getToolbarModule`, `getViewportModule`, etc.)
### Viewport Customization
Custom viewports extend base Cornerstone viewport:
- Override render methods for custom overlays
- Implement measurement tracking
- Add viewport-specific tools and interactions
### Service Integration
Register services in extension's `servicesManager.registerService` and access via:
```javascript
const { MeasurementService } = servicesManager.services;
```
### Creating stores
To create a store, you can make one in your extension's `stores/` directory, and you can follow the example of an existing store such as `useLutPresentationStore.ts` or `useSynchronizersStore.ts`.
### Creating hooks
To create a hook, you can make one in your extension's `hooks/` directory, and you can follow the example of an existing hook such as `usePatientInfo.tsx`.
### Creating providers
To create a provider, you can make one in your extension's `providers/` or `contexts/` directory, and you can follow the example of an existing provider such as `ViewportGridProvider.tsx`.
### Adding new icons
To add a new icon, you can add it to the `icons/` directory, then register the icon using `import { addIcon } from '@ohif/extension-default/src/utils'`
### Creating synchronizers
You can create custom synchronizers and place them in the `synchronizers/` directory, you can follow the example of `frameViewSynchronizer.ts`
### Utilites
Any new utilites should be placed in the `utils/` directory, and you can follow the example of `formatPN.ts`
### Commands
Commands are created in the commandsModule of the extension, for example the cornerstone extension has `commandsModule.tsx`, sometimes its also named `getCommandsModule.tsx.`
### Overriding OHIF Components
To override an OHIF component, you can create a new component in your extension's `components/` directory, then import it instead of the original ui-next component.
### Mode layout
The layoutTemplate is a function that returns a layout object, you can follow the example of `longitudinal/src/index.ts`. This would be helpful when you need to override a component as you can know where to look for the original component.
### Pub Sub
Always prioritrize pub sub, by calling a services subscribe over useEffects as it's more reliable, for example
```ts
useEffect(() => {
const subscriptions = [
cornerstoneViewportService.subscribe(EVENTS.VIEWPORT_DATA_CHANGED, handleViewportDataChanged),
syncGroupService.subscribe(EVENTS.VIEWPORT_REMOVED, onHotKeyRemoval),
syncGroupService.subscribe(EVENTS.VIEWPORT_ADDED, onHotKeyAddition),
];
return () => {
subscriptions.forEach(({ unsubscribe }) => unsubscribe());
};
}, []);
```
### Never modify core architecture
Do not modify the core and always find a way to implement the solution via the extensions and modes, only modify core as a last resort if all other fail or there's an architectural constraint.
## Skills
The `ohif-test-agent` skill (Playwright E2E test guidance) lives at `.agents/skills/ohif-test-agent/`.
## Configuration
### Plugin Configuration
Extensions are auto-discovered via `pluginConfig.json` and dynamically imported during build.
## Medical Imaging Specifics
### DICOM Support
- Multi-format: CT, MRI, X-Ray, Mammography, Ultrasound
- SOP Class handlers for specialized DICOM types (RT, SEG, SR)
- DICOMweb protocol for web-based image retrieval
### Hanging Protocols
Define how images are arranged and displayed:
- Located in `hps/` directories
- JSON configuration with viewport rules
- Support for priors comparison and multi-monitor layouts
### Measurement Tools
- Cornerstone Tools integration for annotations
- Bidirectional measurements, polylines, annotations
- Export capabilities (DICOM SR, CSV reports)
- AI-assisted measurements via ONNX models

View File

@ -3,6 +3,778 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.13.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.88...v3.13.0-beta.89) (2026-06-10)
### Bug Fixes
* ohif tests to run with cornerstone 3d 5.0 ([#6043](https://github.com/OHIF/Viewers/issues/6043)) ([6dd150d](https://github.com/OHIF/Viewers/commit/6dd150d401ad73d60632a23378b7dfd4b5142690))
# [3.13.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.87...v3.13.0-beta.88) (2026-06-05)
### Bug Fixes
* **security:** Patch axios vulnerabilities. ([#6063](https://github.com/OHIF/Viewers/issues/6063)) ([99c6154](https://github.com/OHIF/Viewers/commit/99c615434309b113aed9ac9a977419eeae0528cb))
# [3.13.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.86...v3.13.0-beta.87) (2026-05-29)
**Note:** Version bump only for package ohif-monorepo-root
# [3.13.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.85...v3.13.0-beta.86) (2026-05-28)
**Note:** Version bump only for package ohif-monorepo-root
# [3.13.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.84...v3.13.0-beta.85) (2026-05-28)
### Features
* **WorkList:** New Study List (WorkList based on ui-next); old study list renamed to LegacyWorklist ([#6005](https://github.com/OHIF/Viewers/issues/6005)) ([daae4c1](https://github.com/OHIF/Viewers/commit/daae4c144e7cb80bbee7c05d9fceef8b332ed2a0))
# [3.13.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.83...v3.13.0-beta.84) (2026-05-27)
### Bug Fixes
* **security:** Add tmp as a resolution to fix security vulnerability. ([#6044](https://github.com/OHIF/Viewers/issues/6044)) ([bdcafc1](https://github.com/OHIF/Viewers/commit/bdcafc12de5e4f73a1a96cab9c0830d49315c044))
# [3.13.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.82...v3.13.0-beta.83) (2026-05-26)
### Bug Fixes
* **segmentation:** restore navigation for duplicated contour segments ([#6038](https://github.com/OHIF/Viewers/issues/6038)) ([0c1ca6b](https://github.com/OHIF/Viewers/commit/0c1ca6b04a6c1aef478c7a7d9f360f9b9c50f188))
# [3.13.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.81...v3.13.0-beta.82) (2026-05-22)
**Note:** Version bump only for package ohif-monorepo-root
# [3.13.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.80...v3.13.0-beta.81) (2026-05-21)
### Features
* **tests:** Add tests for duplicating contour segments ([#6002](https://github.com/OHIF/Viewers/issues/6002)) ([2258c79](https://github.com/OHIF/Viewers/commit/2258c7957a18ed0f1a75d2bd5d379006797b2882))
# [3.13.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.79...v3.13.0-beta.80) (2026-05-20)
**Note:** Version bump only for package ohif-monorepo-root
# [3.13.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.78...v3.13.0-beta.79) (2026-05-19)
### Bug Fixes
* **seg:** prevent viewport orientation change when loading SEG in manual grid layout ([#6021](https://github.com/OHIF/Viewers/issues/6021)) ([95251ab](https://github.com/OHIF/Viewers/commit/95251ab6709934e4ef59931379f8d9d7629f87b2))
# [3.13.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.77...v3.13.0-beta.78) (2026-05-19)
### Bug Fixes
* **measurements:** read displayName in SplineROI and PlanarFreehandROI reports ([#5908](https://github.com/OHIF/Viewers/issues/5908)) ([27af682](https://github.com/OHIF/Viewers/commit/27af6821b56f2c5be3a5d4ce38d05be6fbce68f4))
# [3.13.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.76...v3.13.0-beta.77) (2026-05-19)
### Bug Fixes
* **segmentation:** remove thresholdRange label in double-range tool settings ([#5931](https://github.com/OHIF/Viewers/issues/5931)) ([527cfb0](https://github.com/OHIF/Viewers/commit/527cfb0ae8c615aeaddbc7f16892ba4460a5b596))
# [3.13.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.75...v3.13.0-beta.76) (2026-05-19)
**Note:** Version bump only for package ohif-monorepo-root
# [3.13.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.74...v3.13.0-beta.75) (2026-05-18)
**Note:** Version bump only for package ohif-monorepo-root
# [3.13.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.73...v3.13.0-beta.74) (2026-05-15)
### Bug Fixes
* **security:** Update various dependencies to fix security vulnerabilities. ([#6023](https://github.com/OHIF/Viewers/issues/6023)) ([55b46f3](https://github.com/OHIF/Viewers/commit/55b46f39c65a183978a44a61fb322685134f3bd6))
# [3.13.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.72...v3.13.0-beta.73) (2026-05-13)
### Features
* **llm:** add instructions md file for claude and other LLM tools ([#6017](https://github.com/OHIF/Viewers/issues/6017)) ([f231eff](https://github.com/OHIF/Viewers/commit/f231eff5980495c9f3ac0bd907a03388d06c5b90))
# [3.13.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.71...v3.13.0-beta.72) (2026-05-12)
### Bug Fixes
* **measurement-tracking:** restore tracked state on undo after Delete all ([#5994](https://github.com/OHIF/Viewers/issues/5994)) ([397aa4d](https://github.com/OHIF/Viewers/commit/397aa4d0e361e95dcbd83e0557a9cd84c0b8440a))
# [3.13.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.70...v3.13.0-beta.71) (2026-05-07)
### Bug Fixes
* remove waitForVolumeLoad from main toolbar page object ([#6004](https://github.com/OHIF/Viewers/issues/6004)) ([37e19f7](https://github.com/OHIF/Viewers/commit/37e19f734a675a95cedc8a8acc3980816a489574))
# [3.13.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.69...v3.13.0-beta.70) (2026-05-07)
**Note:** Version bump only for package ohif-monorepo-root
# [3.13.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.68...v3.13.0-beta.69) (2026-05-06)
### Bug Fixes
* **security:** Patch axios security vulnerabilities. ([#5998](https://github.com/OHIF/Viewers/issues/5998)) ([5e624f1](https://github.com/OHIF/Viewers/commit/5e624f13870cdb63300b82285b46d4b21b9d47c3))
# [3.13.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.67...v3.13.0-beta.68) (2026-05-05)
### Bug Fixes
* **ToolGroupService:** Centralize tool binding persistence and provide API to add and remove persisted bindings. ([#5989](https://github.com/OHIF/Viewers/issues/5989)) ([979d619](https://github.com/OHIF/Viewers/commit/979d619f7ccb426beb75d59721b01c98175d5241))
# [3.13.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.66...v3.13.0-beta.67) (2026-04-30)
### Features
* **ui-next:** Adds toggle state for ToolButton and Crosshair example ([#5914](https://github.com/OHIF/Viewers/issues/5914)) ([691e267](https://github.com/OHIF/Viewers/commit/691e26731f4f3b3957fe81fc051b09c972696cf6))
# [3.13.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.65...v3.13.0-beta.66) (2026-04-29)
### Bug Fixes
* **config url:** Hardening fetch options. ([#5985](https://github.com/OHIF/Viewers/issues/5985)) ([468e573](https://github.com/OHIF/Viewers/commit/468e5734a9ab02516430cc0dab5d7f2106dea950))
# [3.13.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.64...v3.13.0-beta.65) (2026-04-29)
**Note:** Version bump only for package ohif-monorepo-root
# [3.13.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.63...v3.13.0-beta.64) (2026-04-27)
### Bug Fixes
* **seg:** prevent segmentations from spreading to all viewports before hydration confirmation in 3D four-up ([#5967](https://github.com/OHIF/Viewers/issues/5967)) ([f8ccf9f](https://github.com/OHIF/Viewers/commit/f8ccf9ff2ea9ab7c38bd427514a8ae87902822a3))
# [3.13.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.62...v3.13.0-beta.63) (2026-04-27)
### Bug Fixes
* **security:** refine dynamic config URL trust policy and document trusted-origin behavior ([#5973](https://github.com/OHIF/Viewers/issues/5973)) ([f3cca21](https://github.com/OHIF/Viewers/commit/f3cca21e49729fbf4bab85aa1de64b9dcfebd0ce))
# [3.13.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.61...v3.13.0-beta.62) (2026-04-23)
### Bug Fixes
* **security:** Patch protobufjs for CVE-2026-41242. ([#5974](https://github.com/OHIF/Viewers/issues/5974)) ([1fc97fe](https://github.com/OHIF/Viewers/commit/1fc97fea43cc5e6689bd0076b77a278c67252af7))
# [3.13.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.60...v3.13.0-beta.61) (2026-04-23)
### Features
* **slice scrollbar:** Integrate ViewportSliceProgressScrollbar with customizations and docs ([#5960](https://github.com/OHIF/Viewers/issues/5960)) ([8fc0dc1](https://github.com/OHIF/Viewers/commit/8fc0dc16e97f3262fdac902ecf2d7c20cb8f78cc))
# [3.13.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.59...v3.13.0-beta.60) (2026-04-21)
### Bug Fixes
* **configuration:** Harden dynamic datasource URL trust boundaries and credential handling. ([#5963](https://github.com/OHIF/Viewers/issues/5963)) ([eede569](https://github.com/OHIF/Viewers/commit/eede569a88ed6a3177bbdea8552dfb93ca13cac5))
# [3.13.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.58...v3.13.0-beta.59) (2026-04-16)
### Bug Fixes
* Use newer ONNX version and load without errors ([#5854](https://github.com/OHIF/Viewers/issues/5854)) ([cc5dc20](https://github.com/OHIF/Viewers/commit/cc5dc201649bcbfb4cfad21141dbd56000d30f53)), closes [#5875](https://github.com/OHIF/Viewers/issues/5875) [#5884](https://github.com/OHIF/Viewers/issues/5884) [#5886](https://github.com/OHIF/Viewers/issues/5886) [#5893](https://github.com/OHIF/Viewers/issues/5893) [#5874](https://github.com/OHIF/Viewers/issues/5874) [#5865](https://github.com/OHIF/Viewers/issues/5865)
# [3.13.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.57...v3.13.0-beta.58) (2026-04-14)
**Note:** Version bump only for package ohif-monorepo-root
# [3.13.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.56...v3.13.0-beta.57) (2026-04-13)
### Bug Fixes
* Enhance multiframe instance handling and improve instance valida… ([#5956](https://github.com/OHIF/Viewers/issues/5956)) ([f5a66b9](https://github.com/OHIF/Viewers/commit/f5a66b9eea236cd3291eac857208d7465516c89a))
# [3.13.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.55...v3.13.0-beta.56) (2026-04-10)
### Bug Fixes
* A couple of changes to enable cs3d integration build ([#5944](https://github.com/OHIF/Viewers/issues/5944)) ([f6bbd5c](https://github.com/OHIF/Viewers/commit/f6bbd5c779e4692dc47c93327a890661b8dcc174))
# [3.13.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.54...v3.13.0-beta.55) (2026-04-10)
### Bug Fixes
* ignore auth in git ([#5955](https://github.com/OHIF/Viewers/issues/5955)) ([961eea1](https://github.com/OHIF/Viewers/commit/961eea1c39c37a940c1707c3dfafcede5f4af994))
# [3.13.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.53...v3.13.0-beta.54) (2026-04-08)
### Bug Fixes
* **cornerstone:** read FrameOfReferenceUID from display set in viewport service ([#5950](https://github.com/OHIF/Viewers/issues/5950)) ([d219171](https://github.com/OHIF/Viewers/commit/d219171e25caf5a7c575080f63819d6f4fe45016))
# [3.13.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.52...v3.13.0-beta.53) (2026-04-06)
### Reverts
* rename DisplaySet.frameOfReferenceUID back to FrameOfReferenceUID ([#5943](https://github.com/OHIF/Viewers/issues/5943)) ([0e933c2](https://github.com/OHIF/Viewers/commit/0e933c256e07b7cda35ed2ba3cfb1ab35d895d57))
# [3.13.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.51...v3.13.0-beta.52) (2026-04-06)
### Bug Fixes
* **defaultRouteInit:** pass sorted display sets to hanging protocol for deterministic viewport order ([#5933](https://github.com/OHIF/Viewers/issues/5933)) ([68b10d3](https://github.com/OHIF/Viewers/commit/68b10d365a72d656c83a1c61624dcbd245fff5db))
### Features
* **component:** Adds SmartScrollbar to ui-next - OHIF-2558 ([#5924](https://github.com/OHIF/Viewers/issues/5924)) ([91a8715](https://github.com/OHIF/Viewers/commit/91a8715795c1501b271f10b361382331eb836bf9))
# [3.13.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.50...v3.13.0-beta.51) (2026-04-03)
### Bug Fixes
* **security:** Update yarn.lock that was missed in PR [#5936](https://github.com/OHIF/Viewers/issues/5936). ([#5940](https://github.com/OHIF/Viewers/issues/5940)) ([84ddf78](https://github.com/OHIF/Viewers/commit/84ddf78c879a7cb1aa62c3aaef4428adf67065da))
# [3.13.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.49...v3.13.0-beta.50) (2026-04-02)
### Bug Fixes
* **security:** update dependencies to fix security vulnerabilities ([#5936](https://github.com/OHIF/Viewers/issues/5936)) ([5358a39](https://github.com/OHIF/Viewers/commit/5358a3985a1fa87eb1e3e7493dd61254f790b6d8))
# [3.13.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.48...v3.13.0-beta.49) (2026-04-02)
### Bug Fixes
* **segmentation:** restrict overlay segmentation menu to same frame of reference as viewport background display set ([#5900](https://github.com/OHIF/Viewers/issues/5900)) ([b9029ef](https://github.com/OHIF/Viewers/commit/b9029ef6f8d63a0e36ec929310c1c5ad3f563ef8))
# [3.13.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.47...v3.13.0-beta.48) (2026-04-01)
### Bug Fixes
* **measurement:** Restore viewport interactivity when deleting in-progress Spline or Livewire measurement ([#5905](https://github.com/OHIF/Viewers/issues/5905)) ([7929f08](https://github.com/OHIF/Viewers/commit/7929f0898f1bf882682a912b1c32d93d0944726f))
# [3.13.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.46...v3.13.0-beta.47) (2026-03-28)
### Bug Fixes
* prevent black viewport when navigating series with client-created segmentation ([#5919](https://github.com/OHIF/Viewers/issues/5919)) ([e66fb5a](https://github.com/OHIF/Viewers/commit/e66fb5a2b8c93a60ea20d315f73def7a02ea205f))
# [3.13.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.45...v3.13.0-beta.46) (2026-03-27)
**Note:** Version bump only for package ohif-monorepo-root
# [3.13.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.44...v3.13.0-beta.45) (2026-03-27)
**Note:** Version bump only for package ohif-monorepo-root
# [3.13.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.43...v3.13.0-beta.44) (2026-03-27)
**Note:** Version bump only for package ohif-monorepo-root
# [3.13.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.42...v3.13.0-beta.43) (2026-03-17)
### Bug Fixes
* **SR:** Added support for spline and live wire SR items. ([#5870](https://github.com/OHIF/Viewers/issues/5870)) ([1d4802c](https://github.com/OHIF/Viewers/commit/1d4802c2a38dd75dca7afca87913e100453c9135))
# [3.13.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.41...v3.13.0-beta.42) (2026-03-17)
### Features
* Add combined build ([#5895](https://github.com/OHIF/Viewers/issues/5895)) ([1df671e](https://github.com/OHIF/Viewers/commit/1df671e9ab67d43f98bf09f6838ddc0a5f220277))
# [3.13.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.40...v3.13.0-beta.41) (2026-03-16)
### Bug Fixes
* **sr-hydration:** enable hydration and arrow navigation for 3D SR measurements ([#5887](https://github.com/OHIF/Viewers/issues/5887)) ([7a38903](https://github.com/OHIF/Viewers/commit/7a38903b1977b1cf0f3a0ba8a4a823fac5e8fdeb))
# [3.13.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.39...v3.13.0-beta.40) (2026-03-13)
### Bug Fixes
* **security:** Bump flattened version to address CVE-2026-32141. ([#5897](https://github.com/OHIF/Viewers/issues/5897)) ([d8d376e](https://github.com/OHIF/Viewers/commit/d8d376edaf9c2b7af237182657e3e2203cca5abb))
# [3.13.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.38...v3.13.0-beta.39) (2026-03-13)
### Bug Fixes
* **window level:** The window level value is not displayed by default on all the viewports when selecting common/custom layout and TMTV. ([#5865](https://github.com/OHIF/Viewers/issues/5865)) ([fe1ecfe](https://github.com/OHIF/Viewers/commit/fe1ecfe0cc5d1bcf82686d821f356da8936a8c4b))
# [3.13.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.37...v3.13.0-beta.38) (2026-03-12)
### Bug Fixes
* **segmentation:** Display "No description S:{series number} {modality}" for segmentations with no label. ([#5874](https://github.com/OHIF/Viewers/issues/5874)) ([7b5d0ce](https://github.com/OHIF/Viewers/commit/7b5d0ce40e5422ccfce6091ac3d7559586cbe57e))
# [3.13.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.36...v3.13.0-beta.37) (2026-03-12)
### Bug Fixes
* **security:** Bump tar version to address CVE-2026-31802. ([#5893](https://github.com/OHIF/Viewers/issues/5893)) ([d015b2e](https://github.com/OHIF/Viewers/commit/d015b2e32a418042034c4f557c5a36499c72f702))
# [3.13.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.35...v3.13.0-beta.36) (2026-03-11)
### Bug Fixes
* Modalities in study list should select starts with as primary ([#5886](https://github.com/OHIF/Viewers/issues/5886)) ([b83e978](https://github.com/OHIF/Viewers/commit/b83e978df8c9ffda3759378318267b8d0020653c))
# [3.13.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.34...v3.13.0-beta.35) (2026-03-09)
### Bug Fixes
* **Threshold tool:** Threshold tool no longer becomes deselected when the Dynamic option is selected ([#5884](https://github.com/OHIF/Viewers/issues/5884)) ([889026f](https://github.com/OHIF/Viewers/commit/889026f8a9c20cd21c31b4a02128afd5f057b14b))
# [3.13.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.33...v3.13.0-beta.34) (2026-03-09)
### Bug Fixes
* **seg hydration:** auto-hydrate RT struct on second load with disableConfirmationPrompts ([#5875](https://github.com/OHIF/Viewers/issues/5875)) ([6f773f9](https://github.com/OHIF/Viewers/commit/6f773f997214c388409f3af3e9fc0e5b84debc10))
# [3.13.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.32...v3.13.0-beta.33) (2026-03-05)
### Bug Fixes
* **security:** Bump svgo and tar to fix vulnerabilities. ([#5877](https://github.com/OHIF/Viewers/issues/5877)) ([61a1fcc](https://github.com/OHIF/Viewers/commit/61a1fccd7d47965831a6c80357219eed979ce4d6))
# [3.13.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.31...v3.13.0-beta.32) (2026-03-05)
### Features
* **ecg:** add DICOM ECG waveform extension ([#5856](https://github.com/OHIF/Viewers/issues/5856)) ([70f76ae](https://github.com/OHIF/Viewers/commit/70f76aeba1b27f8e4de5ec73a7b831428e38e3d2))
# [3.13.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.30...v3.13.0-beta.31) (2026-03-04)
### Bug Fixes
* **microscopy:** rename measurement in microscopy mode ([#5866](https://github.com/OHIF/Viewers/issues/5866)) ([2358a73](https://github.com/OHIF/Viewers/commit/2358a73c3cd24953430064480fb9e66bfe36ea69))
# [3.13.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.29...v3.13.0-beta.30) (2026-03-04)
### Bug Fixes
* **segmentation:** segment bidirectional tool error and show toast notification when no segment is drawn ([#5861](https://github.com/OHIF/Viewers/issues/5861)) ([7e10830](https://github.com/OHIF/Viewers/commit/7e10830d5802e22bda4df6a3f8008e9a414b4cd4))
# [3.13.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.28...v3.13.0-beta.29) (2026-03-04)
### Bug Fixes
* **security:** Address various security vulnerabilities. ([#5869](https://github.com/OHIF/Viewers/issues/5869)) ([be8e266](https://github.com/OHIF/Viewers/commit/be8e266a80f13d03ce9dc4a21a0227d3e1bc36ac))
# [3.13.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.27...v3.13.0-beta.28) (2026-03-03)
**Note:** Version bump only for package ohif-monorepo-root
# [3.13.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.26...v3.13.0-beta.27) (2026-03-02)
**Note:** Version bump only for package ohif-monorepo-root
# [3.13.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.25...v3.13.0-beta.26) (2026-02-28)
**Note:** Version bump only for package ohif-monorepo-root
# [3.13.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.24...v3.13.0-beta.25) (2026-02-26)
### Bug Fixes
* Update CS3D 4.18.2 ([#5837](https://github.com/OHIF/Viewers/issues/5837)) ([5370b93](https://github.com/OHIF/Viewers/commit/5370b9382e7bfddeadd69bab9e653f5f997a6e19))
# [3.13.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.23...v3.13.0-beta.24) (2026-02-24)
### Bug Fixes
* prevent viewer crash when opening DICOM Tag Browser from empty viewport ([#5827](https://github.com/OHIF/Viewers/issues/5827)) ([d11d6d4](https://github.com/OHIF/Viewers/commit/d11d6d401bc92a3870ec1089d97235b01a6b27c3))
* **ui-next:** Add validation for opacity and border inputs in the segmentation panel - OHIF-2332 ([#5819](https://github.com/OHIF/Viewers/issues/5819)) ([a8f7099](https://github.com/OHIF/Viewers/commit/a8f709982f16983a11c780e696521df5aa05b604))
# [3.13.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.22...v3.13.0-beta.23) (2026-02-23)
### Bug Fixes
* palette color 8 encoded 16 ([#5823](https://github.com/OHIF/Viewers/issues/5823)) ([eeca1f7](https://github.com/OHIF/Viewers/commit/eeca1f7a7b529d1700f5d281a6fd98074547ffe9)), closes [#5813](https://github.com/OHIF/Viewers/issues/5813)
# [3.13.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.21...v3.13.0-beta.22) (2026-02-23)
**Note:** Version bump only for package ohif-monorepo-root
# [3.13.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.20...v3.13.0-beta.21) (2026-02-23)
### Bug Fixes
* **RTStruct:** RTSTRUCT contours rendered on first slice for multi-frame images without IPP ([#5811](https://github.com/OHIF/Viewers/issues/5811)) ([b5573ca](https://github.com/OHIF/Viewers/commit/b5573caaa65d0d7ff3e83df0ec70b4aa7ea898ca))
# [3.13.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.19...v3.13.0-beta.20) (2026-02-21)
### Bug Fixes
* Published version should match ([#5813](https://github.com/OHIF/Viewers/issues/5813)) ([d359a3b](https://github.com/OHIF/Viewers/commit/d359a3b784227b248c5b09548db363c7f8adbcb8))
# [3.13.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.18...v3.13.0-beta.19) (2026-02-20)
### Bug Fixes
* **security:** CVE-2026-27212 patched. Various dependency updates as a result of CVE-2026-26996. ([#5830](https://github.com/OHIF/Viewers/issues/5830)) ([2722960](https://github.com/OHIF/Viewers/commit/27229609cda3162e37573418ef82ca5d4e365075))
# [3.13.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.17...v3.13.0-beta.18) (2026-02-19)
### Bug Fixes
* **segmentation overlay:** update viewport ds list upon seg delete - OHIF-2425 ([#5729](https://github.com/OHIF/Viewers/issues/5729)) ([b353541](https://github.com/OHIF/Viewers/commit/b35354120a01d89b27844f967c32c052bba8b096))
# [3.13.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.16...v3.13.0-beta.17) (2026-02-19)
### Bug Fixes
* Add copy to clipboard option for capture ([#5720](https://github.com/OHIF/Viewers/issues/5720)) ([53c67f3](https://github.com/OHIF/Viewers/commit/53c67f361234a0b4160c1b11fafcb143a0723336))
# [3.13.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.15...v3.13.0-beta.16) (2026-02-18)
### Bug Fixes
* **security:** Bump tar to 7.5.9 and lerna to 9.0.4 to fix CVE-2026-26960. ([#5824](https://github.com/OHIF/Viewers/issues/5824)) ([3d59c0d](https://github.com/OHIF/Viewers/commit/3d59c0d9d3783336eaa8d37cfc0172818942bf00))
# [3.13.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.14...v3.13.0-beta.15) (2026-02-17) # [3.13.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.14...v3.13.0-beta.15) (2026-02-17)

View File

@ -20,49 +20,33 @@
# #
# syntax=docker/dockerfile:1.7-labs
# This dockerfile is used to publish the `ohif/app` image on dockerhub.
#
# It's a good example of how to build our static application and package it
# with a web server capable of hosting it as static content.
#
# docker build
# --------------
# If you would like to use this dockerfile to build and tag an image, make sure
# you set the context to the project's root directory:
# https://docs.docker.com/engine/reference/commandline/build/
#
#
# SUMMARY
# --------------
# This dockerfile is used as an input for a second stage to make things run faster.
#
# Stage 1: Build the application # Stage 1: Build the application
# docker build -t ohif/viewer:latest . # docker build -t ohif/viewer:latest .
# Copy Files # Copy Files
FROM node:20.18.1-slim as builder FROM node:24.15.0-slim as builder
RUN apt-get update && apt-get install -y build-essential python3 RUN apt-get update && apt-get install -y --no-install-recommends build-essential python3 \
&& rm -rf /var/lib/apt/lists/*
RUN npm install -g pnpm@11
RUN mkdir /usr/src/app RUN mkdir /usr/src/app
WORKDIR /usr/src/app WORKDIR /usr/src/app
RUN npm install -g bun@1.2.23
RUN npm install -g lerna@7.4.2
ENV PATH=/usr/src/app/node_modules/.bin:$PATH ENV PATH=/usr/src/app/node_modules/.bin:$PATH
# Do an initial install and then a final install # Copy package manifests for install caching. preinstall.js is included because
COPY package.json yarn.lock preinstall.js lerna.json ./ # the root package.json's "preinstall" lifecycle script (node preinstall.js)
COPY --parents ./addOns/package.json ./addOns/*/*/package.json ./extensions/*/package.json ./modes/*/package.json ./platform/*/package.json ./ # runs during `pnpm install` below -- before the full source is copied -- so the
# Run the install before copying the rest of the files # script file must already be present or install fails with MODULE_NOT_FOUND.
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc preinstall.js ./
RUN bun pm cache rm COPY --parents ./extensions/*/package.json ./modes/*/package.json ./platform/*/package.json ./
RUN bun install # Run the install before copying the rest of the files.
RUN bun add ajv@8.12.0 # Keep --no-frozen-lockfile here (unlike CI): .dockerignore excludes
# platform/docs, so the lockfile's docs importer has no manifest in the build
# context and a frozen install would fail. pnpm reconciles (drops docs) instead.
RUN pnpm install --no-frozen-lockfile
# Copy the local directory # Copy the local directory
COPY --link --exclude=yarn.lock --exclude=package.json --exclude=Dockerfile . . COPY --link --exclude=pnpm-lock.yaml --exclude=package.json --exclude=Dockerfile . .
# Build here # Build here
# After install it should hopefully be stable until the local directory changes # After install it should hopefully be stable until the local directory changes
@ -72,14 +56,14 @@ ARG APP_CONFIG=config/default.js
ARG PUBLIC_URL=/ ARG PUBLIC_URL=/
ENV PUBLIC_URL=${PUBLIC_URL} ENV PUBLIC_URL=${PUBLIC_URL}
RUN bun run show:config RUN pnpm run show:config
RUN bun run build RUN pnpm run build
# Precompress files # Precompress files
RUN chmod u+x .docker/compressDist.sh RUN chmod u+x .docker/compressDist.sh
RUN ./.docker/compressDist.sh RUN ./.docker/compressDist.sh
# Stage 3: Bundle the built application into a Docker container # Stage 2: Bundle the built application into a Docker container
# which runs Nginx using Alpine Linux # which runs Nginx using Alpine Linux
FROM nginxinc/nginx-unprivileged:1.27-alpine as final FROM nginxinc/nginx-unprivileged:1.27-alpine as final
#RUN apk add --no-cache bash #RUN apk add --no-cache bash

View File

@ -49,6 +49,7 @@ provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-4d.webp?raw=true" alt="4D" width="350"/> | 4D | [Demo](https://viewer.ohif.org/dynamic-volume?StudyInstanceUIDs=2.25.232704420736447710317909004159492840763) | | <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-4d.webp?raw=true" alt="4D" width="350"/> | 4D | [Demo](https://viewer.ohif.org/dynamic-volume?StudyInstanceUIDs=2.25.232704420736447710317909004159492840763) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-video.webp?raw=true" alt="VIDEO" width="350"/> | Video | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.96975534054447904995905761963464388233) | | <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-video.webp?raw=true" alt="VIDEO" width="350"/> | Video | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.96975534054447904995905761963464388233) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/microscopy.webp?raw=true" alt="microscopy" width="350"/> | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.141277760791347900862109212450152067508) | | <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/microscopy.webp?raw=true" alt="microscopy" width="350"/> | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.141277760791347900862109212450152067508) |
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-ecg.webp?raw=true" alt="ECG" width="350"/> | ECG Waveform | [Demo](https://viewer-dev.ohif.org/viewer?StudyInstanceUIDs=2.25.209974489360710696739324151261716440238) |
## About ## About
@ -104,7 +105,6 @@ For commercial support, academic collaborations, and answers to common
questions; please use [Get Support](https://ohif.org/get-support/) to contact questions; please use [Get Support](https://ohif.org/get-support/) to contact
us. us.
## Developing ## Developing
### Branches ### Branches
@ -168,6 +168,64 @@ yarn config set workspaces-experimental true
yarn install --frozen-lockfile yarn install --frozen-lockfile
``` ```
### Cornerstone3D Integration Testing
OHIF's Playwright end-to-end tests can run against a **CS3D branch** or a
**published CS3D version**, allowing changes that span both repositories to be
validated together before merging.
#### Setting up an integration build
1. Add the **`ohif-integration`** label to your OHIF pull request.
2. In the PR body, add a line specifying the CS3D ref:
```
CS3D_REF: feat/my-feature
```
- **Version ref** (e.g. `4.19+`, `4.18.2`) — the workflow resolves it to an
exact published version and swaps the CS3D dependency via npm.
- **Branch ref** (e.g. `main`, `cornerstonejs:feat/foo`) — the workflow
clones the branch, builds CS3D from source with `bun run build:esm`, and
symlinks the built packages into OHIF's `node_modules`.
- For forks, use the `<owner>:<branch>` format
(e.g. `myGithubUser:feat/foo`).
- If no `CS3D_REF` is specified, the default is `4.19+`.
3. The workflow can also be triggered manually via **workflow_dispatch** with a
`cs3d_ref` input.
#### What happens in CI
The [Playwright workflow](.github/workflows/playwright.yml) runs two jobs:
| Job | Purpose |
|-----|---------|
| **Playwright Tests** | Builds OHIF (with CS3D linked or version-swapped), runs the full Playwright suite, uploads test results and coverage, and deploys a Netlify preview when `ohif-integration` is active. |
| **CS3D Branch Merge Guard** | A lightweight check that **fails** when the `ohif-integration` label is present and `CS3D_REF` points to a branch (not a version). This prevents merging while still letting the Playwright tests show green so you can see whether the code actually works. |
#### Testing changes that span both repos
If a feature requires changes in both Cornerstone3D and OHIF:
1. Create your feature branch in CS3D and push it.
2. Create a matching branch in OHIF.
3. Add the `ohif-integration` label to the OHIF pull request.
4. In the PR body, add: `CS3D_REF: <your-cs3d-branch>`.
5. Playwright tests will build CS3D from source, link it, and run the full
suite. The merge guard will block merge until you switch to a published
version — but you can see the test results and the preview deploy while
iterating.
6. Once the CS3D side is merged and published, update the PR body to reference
the published version (e.g. `CS3D_REF: 4.19+`). The tests will run against
the registry version and the merge guard will pass.
#### Preview deploys
When `ohif-integration` is active, the Playwright workflow also builds the OHIF
viewer and deploys it to Netlify as a preview. This gives you a live URL to
manually test the combined CS3D + OHIF changes without running anything locally.
For details on linking CS3D locally for development, see the
[Cornerstone3D README](libs/@cornerstonejs/README.md#local-development-linking--unlinking).
## Commands ## Commands
These commands are available from the root directory. Each project directory These commands are available from the root directory. Each project directory
@ -186,6 +244,32 @@ also supports a number of commands that can be found in their respective
\* - For more information on different builds, check out our [Deploy \* - For more information on different builds, check out our [Deploy
Docs][deployment-docs] Docs][deployment-docs]
### Which config each command uses
The dev server and the production build select a different default
[configuration file][config-file] when `APP_CONFIG` is not set explicitly:
| Command | Default config | Data sources | `?customization=` |
| ------------------------------- | -------------------- | ------------ | ----------------- |
| `dev`, `dev:fast`, `start` | `config/dev.js` | Full set | Enabled |
| `build` | `config/default.js` | One demo source | Disabled |
- **`config/dev.js`** is the full-featured local-development config: every data
source is enabled, the `?customization=` URL feature is turned on (via
`customizationUrlPrefixes`), and it is kept at parity with the public demo
(`config/netlify.js`) so customizations behave locally the same way they do on
the demo.
- **`config/netlify.js`** is the public demo / Netlify deploy config
(`build:viewer:ci`), with the same full data-source set and `?customization=`
enabled.
- **`config/default.js`** is a locked-down baseline and is now **only** the
default for a plain production build (`build` with no `APP_CONFIG`): a single
read-only demo data source and `?customization=` off.
Any explicit `APP_CONFIG` overrides the default, e.g.
`APP_CONFIG=config/default.js pnpm run dev` or
`APP_CONFIG=config/netlify.js pnpm run build`.
## Project ## Project
The OHIF Medical Image Viewing Platform is maintained as a The OHIF Medical Image Viewing Platform is maintained as a
@ -313,6 +397,7 @@ MIT © [OHIF](https://github.com/OHIF)
[ohif-architecture]: https://docs.ohif.org/architecture/index.html [ohif-architecture]: https://docs.ohif.org/architecture/index.html
[ohif-extensions]: https://docs.ohif.org/architecture/index.html [ohif-extensions]: https://docs.ohif.org/architecture/index.html
[deployment-docs]: https://docs.ohif.org/deployment/ [deployment-docs]: https://docs.ohif.org/deployment/
[config-file]: https://docs.ohif.org/configuration/configurationFiles
[react-url]: https://reactjs.org/ [react-url]: https://reactjs.org/
[pwa-url]: https://developers.google.com/web/progressive-web-apps/ [pwa-url]: https://developers.google.com/web/progressive-web-apps/
[ohif-viewer-url]: https://www.npmjs.com/package/@ohif/app [ohif-viewer-url]: https://www.npmjs.com/package/@ohif/app

View File

@ -1,3 +0,0 @@
# External Dependencies
This module contains optional dependencies and external dependencies for including in OHIF, such as the DICOM Microscopy Viewer component.

File diff suppressed because it is too large Load Diff

View File

@ -1,95 +0,0 @@
{
"name": "@externals/devDependencies",
"description": "External dev dependencies - put dev build dependencies here",
"version": "3.13.0-beta.15",
"license": "MIT",
"private": true,
"engines": {
"node": ">=12",
"yarn": ">=1.19.1"
},
"dependencies": {
"@babel/runtime": "7.28.2",
"@kitware/vtk.js": "34.15.1",
"clsx": "2.1.1",
"core-js": "3.45.1",
"moment": "2.30.1"
},
"peerDependencies": {
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@babel/eslint-parser": "7.28.0",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.17",
"@rsbuild/core": "1.5.1",
"@rsbuild/plugin-node-polyfill": "1.4.2",
"@rsbuild/plugin-react": "1.4.0",
"@svgr/webpack": "8.1.0",
"@swc/helpers": "0.5.17",
"@types/jest": "27.5.2",
"@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "6.21.0",
"autoprefixer": "10.4.21",
"babel-loader": "8.4.1",
"babel-plugin-module-resolver": "5.0.2",
"clean-webpack-plugin": "3.0.0",
"copy-webpack-plugin": "9.1.0",
"cross-env": "7.0.3",
"css-loader": "6.11.0",
"dotenv": "8.6.0",
"eslint": "8.57.1",
"eslint-config-prettier": "7.2.0",
"eslint-config-react-app": "6.0.0",
"eslint-plugin-cypress": "2.15.2",
"eslint-plugin-flowtype": "7.0.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-prettier": "5.5.1",
"eslint-plugin-promise": "5.2.0",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-tsdoc": "0.2.17",
"eslint-webpack-plugin": "2.7.0",
"execa": "8.0.1",
"extract-css-chunks-webpack-plugin": "4.10.0",
"html-webpack-plugin": "5.6.3",
"husky": "3.1.0",
"jest": "29.7.0",
"jest-canvas-mock": "2.5.2",
"jest-environment-jsdom": "29.7.0",
"jest-junit": "6.4.0",
"lerna": "7.4.2",
"lint-staged": "9.5.0",
"mini-css-extract-plugin": "2.9.2",
"optimize-css-assets-webpack-plugin": "6.0.1",
"postcss": "8.5.6",
"postcss-import": "14.1.0",
"postcss-loader": "6.2.1",
"postcss-preset-env": "7.8.3",
"prettier": "3.6.2",
"prettier-plugin-tailwindcss": "0.6.9",
"react-refresh": "0.14.2",
"semver": "7.7.2",
"serve": "14.2.4",
"shader-loader": "1.3.1",
"shx": "0.3.4",
"source-map-loader": "4.0.2",
"style-loader": "1.3.0",
"terser-webpack-plugin": "5.3.14",
"typescript": "5.5.4",
"unused-webpack-plugin": "2.4.0",
"webpack": "5.105.0",
"webpack-bundle-analyzer": "4.10.2",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.2.2",
"webpack-hot-middleware": "2.26.1",
"webpack-merge": "5.10.0",
"workbox-webpack-plugin": "6.6.1",
"worker-loader": "3.0.8"
},
"scripts": {
"build": "Included as direct dependency"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +0,0 @@
{
"name": "@externals/dicom-microscopy-viewer",
"description": "External reference to dicom-microscopy-viewer",
"version": "3.13.0-beta.15",
"license": "MIT",
"dependencies": {
"dicom-microscopy-viewer": "0.48.17"
}
}

View File

@ -1,50 +0,0 @@
{
"name": "ohif-monorepo-root",
"private": true,
"packageManager": "yarn@1.22.22",
"workspaces": {
"packages": [
"../platform/i18n",
"../platform/core",
"../platform/ui",
"../platform/ui-next",
"../platform/app",
"../extensions/*",
"../modes/*",
"../addOns/externals/*"
],
"nohoist": [
"**/html-minifier-terser"
]
},
"scripts": {
"preinstall": "cd .. && node preinstall.js"
},
"devDependencies": {
"@babel/core": "7.28.0",
"@babel/plugin-transform-class-properties": "7.27.1",
"@babel/plugin-transform-object-rest-spread": "7.28.0",
"@babel/plugin-transform-private-methods": "7.27.1",
"@babel/plugin-transform-private-property-in-object": "7.27.1",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/plugin-transform-arrow-functions": "7.27.1",
"@babel/plugin-transform-regenerator": "7.28.1",
"@babel/plugin-transform-runtime": "7.28.0",
"@babel/plugin-transform-typescript": "7.28.0",
"@babel/preset-env": "7.28.0",
"@babel/preset-react": "7.27.1",
"@babel/preset-typescript": "7.27.1"
},
"resolutions": {
"**/@babel/runtime": "7.28.2",
"commander": "8.3.0",
"dcmjs": "0.49.4",
"dicomweb-client": "0.10.4",
"nth-check": "2.1.1",
"trim-newlines": "5.0.0",
"glob-parent": "6.0.2",
"trim": "1.0.1",
"package-json": "8.1.1",
"typescript": "5.5.4"
}
}

View File

@ -26,7 +26,9 @@ module.exports = {
'@babel/preset-typescript', '@babel/preset-typescript',
], ],
plugins: [ plugins: [
'babel-plugin-istanbul', // jest's babel coverage provider injects babel-plugin-istanbul when
// --collectCoverage is set; adding it here too makes babel 7 (pulled in
// by jest 30) throw "Duplicate plugin/preset detected".
'@babel/plugin-transform-object-rest-spread', '@babel/plugin-transform-object-rest-spread',
'@babel/plugin-syntax-dynamic-import', '@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-regenerator', '@babel/plugin-transform-regenerator',

7214
bun.lock

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +0,0 @@
[install]
frozenLockfile = true

View File

@ -1,2 +0,0 @@
[install]
frozenLockfile = false

View File

@ -1 +1 @@
cca1a8683bc7d392a3a07268f24585642d13b1cd 6dd150d401ad73d60632a23378b7dfd4b5142690

View File

@ -1,8 +1,8 @@
const webpack = require('webpack'); const webpack = require('@rspack/core');
const { merge } = require('webpack-merge'); const { merge } = require('webpack-merge');
const path = require('path'); const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js'); const webpackCommon = require('./../../../.webpack/webpack.base.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = webpack.CssExtractRspackPlugin;
const pkg = require('./../package.json'); const pkg = require('./../package.json');
@ -35,9 +35,11 @@ module.exports = (env, argv) => {
sideEffects: true, sideEffects: true,
}, },
output: { output: {
library: {
name: 'ohif-extension-cornerstone-dicom-pmap',
type: 'umd',
},
path: ROOT_DIR, path: ROOT_DIR,
library: 'ohif-extension-cornerstone-dicom-pmap',
libraryTarget: 'umd',
filename: pkg.main, filename: pkg.main,
}, },
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],

View File

@ -3,6 +3,619 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.13.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.88...v3.13.0-beta.89) (2026-06-10)
### Bug Fixes
* ohif tests to run with cornerstone 3d 5.0 ([#6043](https://github.com/OHIF/Viewers/issues/6043)) ([6dd150d](https://github.com/OHIF/Viewers/commit/6dd150d401ad73d60632a23378b7dfd4b5142690))
# [3.13.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.87...v3.13.0-beta.88) (2026-06-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.86...v3.13.0-beta.87) (2026-05-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.85...v3.13.0-beta.86) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.84...v3.13.0-beta.85) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.83...v3.13.0-beta.84) (2026-05-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.82...v3.13.0-beta.83) (2026-05-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.81...v3.13.0-beta.82) (2026-05-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.80...v3.13.0-beta.81) (2026-05-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.79...v3.13.0-beta.80) (2026-05-20)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.78...v3.13.0-beta.79) (2026-05-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.77...v3.13.0-beta.78) (2026-05-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.76...v3.13.0-beta.77) (2026-05-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.75...v3.13.0-beta.76) (2026-05-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.74...v3.13.0-beta.75) (2026-05-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.73...v3.13.0-beta.74) (2026-05-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.72...v3.13.0-beta.73) (2026-05-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.71...v3.13.0-beta.72) (2026-05-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.70...v3.13.0-beta.71) (2026-05-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.69...v3.13.0-beta.70) (2026-05-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.68...v3.13.0-beta.69) (2026-05-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.67...v3.13.0-beta.68) (2026-05-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.66...v3.13.0-beta.67) (2026-04-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.65...v3.13.0-beta.66) (2026-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.64...v3.13.0-beta.65) (2026-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.63...v3.13.0-beta.64) (2026-04-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.62...v3.13.0-beta.63) (2026-04-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.61...v3.13.0-beta.62) (2026-04-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.60...v3.13.0-beta.61) (2026-04-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.59...v3.13.0-beta.60) (2026-04-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.58...v3.13.0-beta.59) (2026-04-16)
### Bug Fixes
* Use newer ONNX version and load without errors ([#5854](https://github.com/OHIF/Viewers/issues/5854)) ([cc5dc20](https://github.com/OHIF/Viewers/commit/cc5dc201649bcbfb4cfad21141dbd56000d30f53)), closes [#5875](https://github.com/OHIF/Viewers/issues/5875) [#5884](https://github.com/OHIF/Viewers/issues/5884) [#5886](https://github.com/OHIF/Viewers/issues/5886) [#5893](https://github.com/OHIF/Viewers/issues/5893) [#5874](https://github.com/OHIF/Viewers/issues/5874) [#5865](https://github.com/OHIF/Viewers/issues/5865)
# [3.13.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.57...v3.13.0-beta.58) (2026-04-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.56...v3.13.0-beta.57) (2026-04-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.55...v3.13.0-beta.56) (2026-04-10)
### Bug Fixes
* A couple of changes to enable cs3d integration build ([#5944](https://github.com/OHIF/Viewers/issues/5944)) ([f6bbd5c](https://github.com/OHIF/Viewers/commit/f6bbd5c779e4692dc47c93327a890661b8dcc174))
# [3.13.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.54...v3.13.0-beta.55) (2026-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.53...v3.13.0-beta.54) (2026-04-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.52...v3.13.0-beta.53) (2026-04-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.51...v3.13.0-beta.52) (2026-04-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.50...v3.13.0-beta.51) (2026-04-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.49...v3.13.0-beta.50) (2026-04-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.48...v3.13.0-beta.49) (2026-04-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.47...v3.13.0-beta.48) (2026-04-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.46...v3.13.0-beta.47) (2026-03-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.45...v3.13.0-beta.46) (2026-03-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.44...v3.13.0-beta.45) (2026-03-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.43...v3.13.0-beta.44) (2026-03-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.42...v3.13.0-beta.43) (2026-03-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.41...v3.13.0-beta.42) (2026-03-17)
### Features
* Add combined build ([#5895](https://github.com/OHIF/Viewers/issues/5895)) ([1df671e](https://github.com/OHIF/Viewers/commit/1df671e9ab67d43f98bf09f6838ddc0a5f220277))
# [3.13.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.40...v3.13.0-beta.41) (2026-03-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.39...v3.13.0-beta.40) (2026-03-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.38...v3.13.0-beta.39) (2026-03-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.37...v3.13.0-beta.38) (2026-03-12)
### Bug Fixes
* **segmentation:** Display "No description S:{series number} {modality}" for segmentations with no label. ([#5874](https://github.com/OHIF/Viewers/issues/5874)) ([7b5d0ce](https://github.com/OHIF/Viewers/commit/7b5d0ce40e5422ccfce6091ac3d7559586cbe57e))
# [3.13.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.36...v3.13.0-beta.37) (2026-03-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.35...v3.13.0-beta.36) (2026-03-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.34...v3.13.0-beta.35) (2026-03-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.33...v3.13.0-beta.34) (2026-03-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.32...v3.13.0-beta.33) (2026-03-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.31...v3.13.0-beta.32) (2026-03-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.30...v3.13.0-beta.31) (2026-03-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.29...v3.13.0-beta.30) (2026-03-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.28...v3.13.0-beta.29) (2026-03-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.27...v3.13.0-beta.28) (2026-03-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.26...v3.13.0-beta.27) (2026-03-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.25...v3.13.0-beta.26) (2026-02-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.24...v3.13.0-beta.25) (2026-02-26)
### Bug Fixes
* Update CS3D 4.18.2 ([#5837](https://github.com/OHIF/Viewers/issues/5837)) ([5370b93](https://github.com/OHIF/Viewers/commit/5370b9382e7bfddeadd69bab9e653f5f997a6e19))
# [3.13.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.23...v3.13.0-beta.24) (2026-02-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.22...v3.13.0-beta.23) (2026-02-23)
### Bug Fixes
* palette color 8 encoded 16 ([#5823](https://github.com/OHIF/Viewers/issues/5823)) ([eeca1f7](https://github.com/OHIF/Viewers/commit/eeca1f7a7b529d1700f5d281a6fd98074547ffe9)), closes [#5813](https://github.com/OHIF/Viewers/issues/5813)
# [3.13.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.21...v3.13.0-beta.22) (2026-02-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.20...v3.13.0-beta.21) (2026-02-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.19...v3.13.0-beta.20) (2026-02-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.18...v3.13.0-beta.19) (2026-02-20)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.17...v3.13.0-beta.18) (2026-02-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.16...v3.13.0-beta.17) (2026-02-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.15...v3.13.0-beta.16) (2026-02-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap
# [3.13.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.14...v3.13.0-beta.15) (2026-02-17) # [3.13.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.14...v3.13.0-beta.15) (2026-02-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap **Note:** Version bump only for package @ohif/extension-cornerstone-dicom-pmap

View File

@ -1,42 +1,37 @@
{ {
"name": "@ohif/extension-cornerstone-dicom-pmap", "name": "@ohif/extension-cornerstone-dicom-pmap",
"version": "3.13.0-beta.15", "version": "3.13.0-beta.125",
"description": "DICOM Parametric Map read workflow", "description": "DICOM Parametric Map read workflow",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
"repository": "OHIF/Viewers",
"main": "dist/ohif-extension-cornerstone-dicom-pmap.umd.js", "main": "dist/ohif-extension-cornerstone-dicom-pmap.umd.js",
"module": "src/index.tsx", "module": "src/index.tsx",
"engines": {
"node": ">=24"
},
"files": [ "files": [
"dist/**", "dist/**",
"public/**", "public/**",
"README.md" "README.md"
], ],
"repository": "OHIF/Viewers",
"keywords": [
"ohif-extension"
],
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1.18.0"
},
"scripts": { "scripts": {
"clean": "shx rm -rf dist", "clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules", "clean:deep": "pnpm run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", "dev": "cross-env NODE_ENV=development rspack build --config .webpack/webpack.dev.js --watch",
"dev:dicom-pmap": "yarn run dev", "dev:dicom-pmap": "pnpm run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", "build": "cross-env NODE_ENV=production rspack build --config .webpack/webpack.prod.js",
"build:package-1": "yarn run build", "build:package-1": "pnpm run build",
"start": "yarn run dev" "start": "pnpm run dev"
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.13.0-beta.15", "@ohif/core": "workspace:*",
"@ohif/extension-cornerstone": "3.13.0-beta.15", "@ohif/extension-cornerstone": "workspace:*",
"@ohif/extension-default": "3.13.0-beta.15", "@ohif/extension-default": "workspace:*",
"@ohif/i18n": "3.13.0-beta.15", "@ohif/i18n": "workspace:*",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
@ -45,9 +40,15 @@
"react-router-dom": "6.30.3" "react-router-dom": "6.30.3"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "7.28.2", "@babel/runtime": "7.29.7",
"@cornerstonejs/adapters": "4.15.29", "@cornerstonejs/adapters": "5.4.17",
"@cornerstonejs/core": "4.15.29", "@cornerstonejs/core": "5.4.17",
"@kitware/vtk.js": "34.15.1" "@kitware/vtk.js": "35.5.3"
} },
"devDependencies": {
"cross-env": "7.0.3"
},
"keywords": [
"ohif-extension"
]
} }

View File

@ -1,4 +1,4 @@
const webpack = require('webpack'); const webpack = require('@rspack/core');
const { merge } = require('webpack-merge'); const { merge } = require('webpack-merge');
const path = require('path'); const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js'); const webpackCommon = require('./../../../.webpack/webpack.base.js');
@ -32,9 +32,11 @@ module.exports = (env, argv) => {
sideEffects: false, sideEffects: false,
}, },
output: { output: {
library: {
name: 'ohif-extension-cornerstone-dicom-rt',
type: 'umd',
},
path: ROOT_DIR, path: ROOT_DIR,
library: 'ohif-extension-cornerstone-dicom-rt',
libraryTarget: 'umd',
filename: pkg.main, filename: pkg.main,
}, },
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],

View File

@ -3,6 +3,607 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.13.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.88...v3.13.0-beta.89) (2026-06-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.87...v3.13.0-beta.88) (2026-06-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.86...v3.13.0-beta.87) (2026-05-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.85...v3.13.0-beta.86) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.84...v3.13.0-beta.85) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.83...v3.13.0-beta.84) (2026-05-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.82...v3.13.0-beta.83) (2026-05-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.81...v3.13.0-beta.82) (2026-05-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.80...v3.13.0-beta.81) (2026-05-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.79...v3.13.0-beta.80) (2026-05-20)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.78...v3.13.0-beta.79) (2026-05-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.77...v3.13.0-beta.78) (2026-05-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.76...v3.13.0-beta.77) (2026-05-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.75...v3.13.0-beta.76) (2026-05-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.74...v3.13.0-beta.75) (2026-05-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.73...v3.13.0-beta.74) (2026-05-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.72...v3.13.0-beta.73) (2026-05-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.71...v3.13.0-beta.72) (2026-05-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.70...v3.13.0-beta.71) (2026-05-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.69...v3.13.0-beta.70) (2026-05-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.68...v3.13.0-beta.69) (2026-05-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.67...v3.13.0-beta.68) (2026-05-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.66...v3.13.0-beta.67) (2026-04-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.65...v3.13.0-beta.66) (2026-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.64...v3.13.0-beta.65) (2026-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.63...v3.13.0-beta.64) (2026-04-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.62...v3.13.0-beta.63) (2026-04-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.61...v3.13.0-beta.62) (2026-04-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.60...v3.13.0-beta.61) (2026-04-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.59...v3.13.0-beta.60) (2026-04-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.58...v3.13.0-beta.59) (2026-04-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.57...v3.13.0-beta.58) (2026-04-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.56...v3.13.0-beta.57) (2026-04-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.55...v3.13.0-beta.56) (2026-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.54...v3.13.0-beta.55) (2026-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.53...v3.13.0-beta.54) (2026-04-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.52...v3.13.0-beta.53) (2026-04-06)
### Reverts
* rename DisplaySet.frameOfReferenceUID back to FrameOfReferenceUID ([#5943](https://github.com/OHIF/Viewers/issues/5943)) ([0e933c2](https://github.com/OHIF/Viewers/commit/0e933c256e07b7cda35ed2ba3cfb1ab35d895d57))
# [3.13.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.51...v3.13.0-beta.52) (2026-04-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.50...v3.13.0-beta.51) (2026-04-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.49...v3.13.0-beta.50) (2026-04-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.48...v3.13.0-beta.49) (2026-04-02)
### Bug Fixes
* **segmentation:** restrict overlay segmentation menu to same frame of reference as viewport background display set ([#5900](https://github.com/OHIF/Viewers/issues/5900)) ([b9029ef](https://github.com/OHIF/Viewers/commit/b9029ef6f8d63a0e36ec929310c1c5ad3f563ef8))
# [3.13.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.47...v3.13.0-beta.48) (2026-04-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.46...v3.13.0-beta.47) (2026-03-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.45...v3.13.0-beta.46) (2026-03-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.44...v3.13.0-beta.45) (2026-03-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.43...v3.13.0-beta.44) (2026-03-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.42...v3.13.0-beta.43) (2026-03-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.41...v3.13.0-beta.42) (2026-03-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.40...v3.13.0-beta.41) (2026-03-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.39...v3.13.0-beta.40) (2026-03-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.38...v3.13.0-beta.39) (2026-03-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.37...v3.13.0-beta.38) (2026-03-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.36...v3.13.0-beta.37) (2026-03-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.35...v3.13.0-beta.36) (2026-03-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.34...v3.13.0-beta.35) (2026-03-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.33...v3.13.0-beta.34) (2026-03-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.32...v3.13.0-beta.33) (2026-03-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.31...v3.13.0-beta.32) (2026-03-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.30...v3.13.0-beta.31) (2026-03-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.29...v3.13.0-beta.30) (2026-03-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.28...v3.13.0-beta.29) (2026-03-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.27...v3.13.0-beta.28) (2026-03-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.26...v3.13.0-beta.27) (2026-03-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.25...v3.13.0-beta.26) (2026-02-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.24...v3.13.0-beta.25) (2026-02-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.23...v3.13.0-beta.24) (2026-02-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.22...v3.13.0-beta.23) (2026-02-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.21...v3.13.0-beta.22) (2026-02-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.20...v3.13.0-beta.21) (2026-02-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.19...v3.13.0-beta.20) (2026-02-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.18...v3.13.0-beta.19) (2026-02-20)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.17...v3.13.0-beta.18) (2026-02-19)
### Bug Fixes
* **segmentation overlay:** update viewport ds list upon seg delete - OHIF-2425 ([#5729](https://github.com/OHIF/Viewers/issues/5729)) ([b353541](https://github.com/OHIF/Viewers/commit/b35354120a01d89b27844f967c32c052bba8b096))
# [3.13.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.16...v3.13.0-beta.17) (2026-02-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.15...v3.13.0-beta.16) (2026-02-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
# [3.13.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.14...v3.13.0-beta.15) (2026-02-17) # [3.13.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.14...v3.13.0-beta.15) (2026-02-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt **Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt

View File

@ -1,11 +1,15 @@
{ {
"name": "@ohif/extension-cornerstone-dicom-rt", "name": "@ohif/extension-cornerstone-dicom-rt",
"version": "3.13.0-beta.15", "version": "3.13.0-beta.125",
"description": "DICOM RT read workflow", "description": "DICOM RT read workflow",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
"repository": "OHIF/Viewers",
"main": "dist/ohif-extension-cornerstone-dicom-rt.umd.js", "main": "dist/ohif-extension-cornerstone-dicom-rt.umd.js",
"module": "src/index.tsx", "module": "src/index.tsx",
"engines": {
"node": ">=24"
},
"files": [ "files": [
"dist/**", "dist/**",
"public/**", "public/**",
@ -14,29 +18,20 @@
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
"repository": "OHIF/Viewers",
"keywords": [
"ohif-extension"
],
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1.18.0"
},
"scripts": { "scripts": {
"clean": "shx rm -rf dist", "clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules", "clean:deep": "pnpm run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", "dev": "cross-env NODE_ENV=development rspack build --config .webpack/webpack.dev.js --watch",
"dev:dicom-seg": "yarn run dev", "dev:dicom-seg": "pnpm run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", "build": "cross-env NODE_ENV=production rspack build --config .webpack/webpack.prod.js",
"build:package-1": "yarn run build", "build:package-1": "pnpm run build",
"start": "yarn run dev" "start": "pnpm run dev"
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.13.0-beta.15", "@ohif/core": "workspace:*",
"@ohif/extension-cornerstone": "3.13.0-beta.15", "@ohif/extension-cornerstone": "workspace:*",
"@ohif/extension-default": "3.13.0-beta.15", "@ohif/extension-default": "workspace:*",
"@ohif/i18n": "3.13.0-beta.15", "@ohif/i18n": "workspace:*",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
@ -45,6 +40,12 @@
"react-router-dom": "6.30.3" "react-router-dom": "6.30.3"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "7.28.2" "@babel/runtime": "7.29.7"
} },
"devDependencies": {
"cross-env": "7.0.3"
},
"keywords": [
"ohif-extension"
]
} }

View File

@ -1,5 +1,6 @@
import { utils, Types as OhifTypes } from '@ohif/core'; import { utils, Types as OhifTypes } from '@ohif/core';
import i18n from '@ohif/i18n'; import i18n from '@ohif/i18n';
import { segmentation as cstSegmentation } from '@cornerstonejs/tools';
import { SOPClassHandlerId } from './id'; import { SOPClassHandlerId } from './id';
import loadRTStruct from './loadRTStruct'; import loadRTStruct from './loadRTStruct';
@ -8,8 +9,6 @@ const { sopClassDictionary } = utils;
const sopClassUids = [sopClassDictionary.RTStructureSetStorage]; const sopClassUids = [sopClassDictionary.RTStructureSetStorage];
const cachedRTStructsSEG = new Set<string>();
const loadPromises = {}; const loadPromises = {};
function _getDisplaySetsFromSeries( function _getDisplaySetsFromSeries(
@ -57,6 +56,7 @@ function _getDisplaySetsFromSeries(
StudyInstanceUID, StudyInstanceUID,
SOPClassHandlerId, SOPClassHandlerId,
SOPClassUID, SOPClassUID,
FrameOfReferenceUID: null,
referencedImages: null, referencedImages: null,
referencedSeriesInstanceUID: null, referencedSeriesInstanceUID: null,
referencedDisplaySetInstanceUID: null, referencedDisplaySetInstanceUID: null,
@ -96,6 +96,9 @@ function _getDisplaySetsFromSeries(
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence; displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID; displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
displaySet.FrameOfReferenceUID =
instance.ReferencedFrameOfReferenceSequence?.[0]?.FrameOfReferenceUID;
const { displaySetService } = servicesManager.services; const { displaySetService } = servicesManager.services;
const referencedDisplaySets = const referencedDisplaySets =
displaySetService.getDisplaySetsForReferences(referencedSeriesSequence); displaySetService.getDisplaySetsForReferences(referencedSeriesSequence);
@ -114,6 +117,7 @@ function _getDisplaySetsFromSeries(
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) { if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID; displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
displaySet.isReconstructable = addedDisplaySet.isReconstructable; displaySet.isReconstructable = addedDisplaySet.isReconstructable;
displaySet.FrameOfReferenceUID = addedDisplaySet.FrameOfReferenceUID;
unsubscribe(); unsubscribe();
} }
} }
@ -122,6 +126,7 @@ function _getDisplaySetsFromSeries(
const [referencedDisplaySet] = referencedDisplaySets; const [referencedDisplaySet] = referencedDisplaySets;
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID; displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
displaySet.isReconstructable = referencedDisplaySet.isReconstructable; displaySet.isReconstructable = referencedDisplaySet.isReconstructable;
displaySet.FrameOfReferenceUID = referencedDisplaySet.FrameOfReferenceUID;
} }
displaySet.load = ({ headers, createSegmentation = true }) => displaySet.load = ({ headers, createSegmentation = true }) =>
@ -143,23 +148,13 @@ function _load(
if ( if (
(rtDisplaySet.loading || rtDisplaySet.isLoaded) && (rtDisplaySet.loading || rtDisplaySet.isLoaded) &&
loadPromises[SOPInstanceUID] && loadPromises[SOPInstanceUID] &&
cachedRTStructsSEG.has(rtDisplaySet.displaySetInstanceUID) _segmentationExists(rtDisplaySet)
) { ) {
return loadPromises[SOPInstanceUID]; return loadPromises[SOPInstanceUID];
} }
rtDisplaySet.loading = true; rtDisplaySet.loading = true;
const { unsubscribe } = segmentationService.subscribe(
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
(evt: { rtDisplaySet: { displaySetInstanceUID: string } }) => {
if (evt.rtDisplaySet?.displaySetInstanceUID === rtDisplaySet.displaySetInstanceUID) {
cachedRTStructsSEG.add(rtDisplaySet.displaySetInstanceUID);
unsubscribe();
}
}
);
// We don't want to fire multiple loads, so we'll wait for the first to finish // We don't want to fire multiple loads, so we'll wait for the first to finish
// and also return the same promise to any other callers. // and also return the same promise to any other callers.
loadPromises[SOPInstanceUID] = new Promise<void>(async (resolve, reject) => { loadPromises[SOPInstanceUID] = new Promise<void>(async (resolve, reject) => {
@ -219,6 +214,10 @@ function _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
return ReferencedSeriesSequence; return ReferencedSeriesSequence;
} }
function _segmentationExists(segDisplaySet) {
return !!cstSegmentation.state.getSegmentation(segDisplaySet.displaySetInstanceUID);
}
function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) { function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) {
const { servicesManager, extensionManager } = params; const { servicesManager, extensionManager } = params;

View File

@ -168,7 +168,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) {
return () => { return () => {
// remove the segmentation representations if seg displayset changed // remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentations(viewportId); segmentationService.removeRepresentationsFromViewport(viewportId);
referencedDisplaySetRef.current = null; referencedDisplaySetRef.current = null;
toolGroupService.destroyToolGroup(toolGroupId); toolGroupService.destroyToolGroup(toolGroupId);
}; };

View File

@ -1,8 +1,8 @@
const webpack = require('webpack'); const webpack = require('@rspack/core');
const { merge } = require('webpack-merge'); const { merge } = require('webpack-merge');
const path = require('path'); const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js'); const webpackCommon = require('./../../../.webpack/webpack.base.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = webpack.CssExtractRspackPlugin;
const pkg = require('./../package.json'); const pkg = require('./../package.json');
@ -35,9 +35,11 @@ module.exports = (env, argv) => {
sideEffects: true, sideEffects: true,
}, },
output: { output: {
library: {
name: 'ohif-extension-cornerstone-dicom-seg',
type: 'umd',
},
path: ROOT_DIR, path: ROOT_DIR,
library: 'ohif-extension-cornerstone-dicom-seg',
libraryTarget: 'umd',
filename: pkg.main, filename: pkg.main,
}, },
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],

View File

@ -3,6 +3,628 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.13.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.88...v3.13.0-beta.89) (2026-06-10)
### Bug Fixes
* ohif tests to run with cornerstone 3d 5.0 ([#6043](https://github.com/OHIF/Viewers/issues/6043)) ([6dd150d](https://github.com/OHIF/Viewers/commit/6dd150d401ad73d60632a23378b7dfd4b5142690))
# [3.13.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.87...v3.13.0-beta.88) (2026-06-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.86...v3.13.0-beta.87) (2026-05-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.85...v3.13.0-beta.86) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.84...v3.13.0-beta.85) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.83...v3.13.0-beta.84) (2026-05-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.82...v3.13.0-beta.83) (2026-05-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.81...v3.13.0-beta.82) (2026-05-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.80...v3.13.0-beta.81) (2026-05-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.79...v3.13.0-beta.80) (2026-05-20)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.78...v3.13.0-beta.79) (2026-05-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.77...v3.13.0-beta.78) (2026-05-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.76...v3.13.0-beta.77) (2026-05-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.75...v3.13.0-beta.76) (2026-05-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.74...v3.13.0-beta.75) (2026-05-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.73...v3.13.0-beta.74) (2026-05-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.72...v3.13.0-beta.73) (2026-05-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.71...v3.13.0-beta.72) (2026-05-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.70...v3.13.0-beta.71) (2026-05-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.69...v3.13.0-beta.70) (2026-05-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.68...v3.13.0-beta.69) (2026-05-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.67...v3.13.0-beta.68) (2026-05-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.66...v3.13.0-beta.67) (2026-04-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.65...v3.13.0-beta.66) (2026-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.64...v3.13.0-beta.65) (2026-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.63...v3.13.0-beta.64) (2026-04-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.62...v3.13.0-beta.63) (2026-04-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.61...v3.13.0-beta.62) (2026-04-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.60...v3.13.0-beta.61) (2026-04-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.59...v3.13.0-beta.60) (2026-04-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.58...v3.13.0-beta.59) (2026-04-16)
### Bug Fixes
* Use newer ONNX version and load without errors ([#5854](https://github.com/OHIF/Viewers/issues/5854)) ([cc5dc20](https://github.com/OHIF/Viewers/commit/cc5dc201649bcbfb4cfad21141dbd56000d30f53)), closes [#5875](https://github.com/OHIF/Viewers/issues/5875) [#5884](https://github.com/OHIF/Viewers/issues/5884) [#5886](https://github.com/OHIF/Viewers/issues/5886) [#5893](https://github.com/OHIF/Viewers/issues/5893) [#5874](https://github.com/OHIF/Viewers/issues/5874) [#5865](https://github.com/OHIF/Viewers/issues/5865)
# [3.13.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.57...v3.13.0-beta.58) (2026-04-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.56...v3.13.0-beta.57) (2026-04-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.55...v3.13.0-beta.56) (2026-04-10)
### Bug Fixes
* A couple of changes to enable cs3d integration build ([#5944](https://github.com/OHIF/Viewers/issues/5944)) ([f6bbd5c](https://github.com/OHIF/Viewers/commit/f6bbd5c779e4692dc47c93327a890661b8dcc174))
# [3.13.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.54...v3.13.0-beta.55) (2026-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.53...v3.13.0-beta.54) (2026-04-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.52...v3.13.0-beta.53) (2026-04-06)
### Reverts
* rename DisplaySet.frameOfReferenceUID back to FrameOfReferenceUID ([#5943](https://github.com/OHIF/Viewers/issues/5943)) ([0e933c2](https://github.com/OHIF/Viewers/commit/0e933c256e07b7cda35ed2ba3cfb1ab35d895d57))
# [3.13.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.51...v3.13.0-beta.52) (2026-04-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.50...v3.13.0-beta.51) (2026-04-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.49...v3.13.0-beta.50) (2026-04-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.48...v3.13.0-beta.49) (2026-04-02)
### Bug Fixes
* **segmentation:** restrict overlay segmentation menu to same frame of reference as viewport background display set ([#5900](https://github.com/OHIF/Viewers/issues/5900)) ([b9029ef](https://github.com/OHIF/Viewers/commit/b9029ef6f8d63a0e36ec929310c1c5ad3f563ef8))
# [3.13.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.47...v3.13.0-beta.48) (2026-04-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.46...v3.13.0-beta.47) (2026-03-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.45...v3.13.0-beta.46) (2026-03-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.44...v3.13.0-beta.45) (2026-03-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.43...v3.13.0-beta.44) (2026-03-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.42...v3.13.0-beta.43) (2026-03-17)
### Bug Fixes
* **SR:** Added support for spline and live wire SR items. ([#5870](https://github.com/OHIF/Viewers/issues/5870)) ([1d4802c](https://github.com/OHIF/Viewers/commit/1d4802c2a38dd75dca7afca87913e100453c9135))
# [3.13.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.41...v3.13.0-beta.42) (2026-03-17)
### Features
* Add combined build ([#5895](https://github.com/OHIF/Viewers/issues/5895)) ([1df671e](https://github.com/OHIF/Viewers/commit/1df671e9ab67d43f98bf09f6838ddc0a5f220277))
# [3.13.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.40...v3.13.0-beta.41) (2026-03-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.39...v3.13.0-beta.40) (2026-03-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.38...v3.13.0-beta.39) (2026-03-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.37...v3.13.0-beta.38) (2026-03-12)
### Bug Fixes
* **segmentation:** Display "No description S:{series number} {modality}" for segmentations with no label. ([#5874](https://github.com/OHIF/Viewers/issues/5874)) ([7b5d0ce](https://github.com/OHIF/Viewers/commit/7b5d0ce40e5422ccfce6091ac3d7559586cbe57e))
# [3.13.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.36...v3.13.0-beta.37) (2026-03-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.35...v3.13.0-beta.36) (2026-03-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.34...v3.13.0-beta.35) (2026-03-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.33...v3.13.0-beta.34) (2026-03-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.32...v3.13.0-beta.33) (2026-03-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.31...v3.13.0-beta.32) (2026-03-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.30...v3.13.0-beta.31) (2026-03-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.29...v3.13.0-beta.30) (2026-03-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.28...v3.13.0-beta.29) (2026-03-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.27...v3.13.0-beta.28) (2026-03-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.26...v3.13.0-beta.27) (2026-03-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.25...v3.13.0-beta.26) (2026-02-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.24...v3.13.0-beta.25) (2026-02-26)
### Bug Fixes
* Update CS3D 4.18.2 ([#5837](https://github.com/OHIF/Viewers/issues/5837)) ([5370b93](https://github.com/OHIF/Viewers/commit/5370b9382e7bfddeadd69bab9e653f5f997a6e19))
# [3.13.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.23...v3.13.0-beta.24) (2026-02-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.22...v3.13.0-beta.23) (2026-02-23)
### Bug Fixes
* palette color 8 encoded 16 ([#5823](https://github.com/OHIF/Viewers/issues/5823)) ([eeca1f7](https://github.com/OHIF/Viewers/commit/eeca1f7a7b529d1700f5d281a6fd98074547ffe9)), closes [#5813](https://github.com/OHIF/Viewers/issues/5813)
# [3.13.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.21...v3.13.0-beta.22) (2026-02-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.20...v3.13.0-beta.21) (2026-02-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.19...v3.13.0-beta.20) (2026-02-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.18...v3.13.0-beta.19) (2026-02-20)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.17...v3.13.0-beta.18) (2026-02-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.16...v3.13.0-beta.17) (2026-02-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.15...v3.13.0-beta.16) (2026-02-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
# [3.13.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.14...v3.13.0-beta.15) (2026-02-17) # [3.13.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.14...v3.13.0-beta.15) (2026-02-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg **Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg

View File

@ -1,42 +1,37 @@
{ {
"name": "@ohif/extension-cornerstone-dicom-seg", "name": "@ohif/extension-cornerstone-dicom-seg",
"version": "3.13.0-beta.15", "version": "3.13.0-beta.125",
"description": "DICOM SEG read workflow", "description": "DICOM SEG read workflow",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
"repository": "OHIF/Viewers",
"main": "dist/ohif-extension-cornerstone-dicom-seg.umd.js", "main": "dist/ohif-extension-cornerstone-dicom-seg.umd.js",
"module": "src/index.tsx", "module": "src/index.tsx",
"engines": {
"node": ">=24"
},
"files": [ "files": [
"dist/**", "dist/**",
"public/**", "public/**",
"README.md" "README.md"
], ],
"repository": "OHIF/Viewers",
"keywords": [
"ohif-extension"
],
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1.18.0"
},
"scripts": { "scripts": {
"clean": "shx rm -rf dist", "clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules", "clean:deep": "pnpm run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", "dev": "cross-env NODE_ENV=development rspack build --config .webpack/webpack.dev.js --watch",
"dev:dicom-seg": "yarn run dev", "dev:dicom-seg": "pnpm run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", "build": "cross-env NODE_ENV=production rspack build --config .webpack/webpack.prod.js",
"build:package-1": "yarn run build", "build:package-1": "pnpm run build",
"start": "yarn run dev" "start": "pnpm run dev"
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.13.0-beta.15", "@ohif/core": "workspace:*",
"@ohif/extension-cornerstone": "3.13.0-beta.15", "@ohif/extension-cornerstone": "workspace:*",
"@ohif/extension-default": "3.13.0-beta.15", "@ohif/extension-default": "workspace:*",
"@ohif/i18n": "3.13.0-beta.15", "@ohif/i18n": "workspace:*",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
@ -45,9 +40,15 @@
"react-router-dom": "6.30.3" "react-router-dom": "6.30.3"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "7.28.2", "@babel/runtime": "7.29.7",
"@cornerstonejs/adapters": "4.15.29", "@cornerstonejs/adapters": "5.4.17",
"@cornerstonejs/core": "4.15.29", "@cornerstonejs/core": "5.4.17",
"@kitware/vtk.js": "34.15.1" "@kitware/vtk.js": "35.5.3"
} },
"devDependencies": {
"cross-env": "7.0.3"
},
"keywords": [
"ohif-extension"
]
} }

View File

@ -2,11 +2,15 @@ import dcmjs from 'dcmjs';
import { classes, Types, utils } from '@ohif/core'; import { classes, Types, utils } from '@ohif/core';
import { cache, metaData } from '@cornerstonejs/core'; import { cache, metaData } from '@cornerstonejs/core';
import { segmentation as cornerstoneToolsSegmentation } from '@cornerstonejs/tools'; import { segmentation as cornerstoneToolsSegmentation } from '@cornerstonejs/tools';
import { adaptersRT, helpers, adaptersSEG } from '@cornerstonejs/adapters'; import { adaptersRT, adaptersSEG } from '@cornerstonejs/adapters';
import { createReportDialogPrompt, useUIStateStore } from '@ohif/extension-default'; import { createReportDialogPrompt, useUIStateStore } from '@ohif/extension-default';
import { DicomMetadataStore } from '@ohif/core';
import PROMPT_RESPONSES from '../../default/src/utils/_shared/PROMPT_RESPONSES'; import PROMPT_RESPONSES from '../../default/src/utils/_shared/PROMPT_RESPONSES';
import {
getSegmentationSaveOptions,
LABELMAP_SEG_SOP_CLASS_UID,
BITMAP_SEG_SOP_CLASS_UID,
} from './utils/segmentationConfig';
const getTargetViewport = ({ viewportId, viewportGridService }) => { const getTargetViewport = ({ viewportId, viewportGridService }) => {
const { viewports, activeViewportId } = viewportGridService.getState(); const { viewports, activeViewportId } = viewportGridService.getState();
@ -29,13 +33,12 @@ const {
}, },
} = adaptersRT; } = adaptersRT;
const { downloadDICOMData } = helpers;
const commandsModule = ({ const commandsModule = ({
servicesManager, servicesManager,
extensionManager, extensionManager,
commandsManager,
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => { }: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
const { segmentationService, displaySetService, viewportGridService } = const { segmentationService, displaySetService, viewportGridService, customizationService } =
servicesManager.services as AppTypes.Services; servicesManager.services as AppTypes.Services;
const actions = { const actions = {
@ -89,49 +92,85 @@ const commandsModule = ({
* @returns Returns the generated segmentation data. * @returns Returns the generated segmentation data.
*/ */
generateSegmentation: ({ segmentationId, options = {} }) => { generateSegmentation: ({ segmentationId, options = {} }) => {
// `dataSource` (a data source name) is consumed here to resolve the store
// overrides; it must not be forwarded to the adapter's generateSegmentation.
const { dataSource: dataSourceName, ...generateOptions } = options;
const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId); const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId);
const predecessorImageId = options.predecessorImageId ?? segmentation.predecessorImageId; const predecessorImageId =
generateOptions.predecessorImageId ?? segmentation.predecessorImageId;
const { imageIds } = segmentation.representationData.Labelmap; // A data source may override the app-wide `segmentation.store.*` defaults
// via `configuration.segmentation.store` (different back ends support
// different SEG encodings). Use the named target data source when storing,
// otherwise the active one (e.g. download).
const dataSourceDefinition = dataSourceName
? extensionManager.getDataSourceDefinition(dataSourceName)
: extensionManager.getActiveDataSourceDefinition();
const dataSourceStoreOverride = dataSourceDefinition?.configuration?.segmentation?.store;
const segImages = imageIds.map(imageId => cache.getImage(imageId)); const labelmapData = segmentation.representationData.Labelmap;
const referencedImages = segImages.map(image => cache.getImage(image.referencedImageId));
const labelmaps2D = []; // 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 = [];
let z = 0; // 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;
for (const segImage of segImages) { let z = 0;
const segmentsOnLabelmap = new Set();
const pixelData = segImage.getPixelData();
const { rows, columns } = segImage;
// Use a single pass through the pixel data for (const segImage of segImages) {
for (let i = 0; i < pixelData.length; i++) { const segmentsOnLabelmap = new Set();
const segment = pixelData[i]; const pixelData = segImage.getPixelData();
if (segment !== 0) { const { rows, columns } = segImage;
segmentsOnLabelmap.add(segment);
// 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);
}
} }
const frameIndex = referencedFrameIndexById
? referencedFrameIndexById.get(segImage.referencedImageId) ?? -1
: z++;
if (frameIndex < 0) {
continue;
}
labelmaps2D[frameIndex] = {
segmentsOnLabelmap: Array.from(segmentsOnLabelmap),
pixelData,
rows,
columns,
};
} }
labelmaps2D[z++] = { const allSegmentsOnLabelmap = labelmaps2D
segmentsOnLabelmap: Array.from(segmentsOnLabelmap), .filter(Boolean)
pixelData, .map(labelmap => labelmap.segmentsOnLabelmap);
rows,
columns, return {
segmentsOnLabelmap: Array.from(new Set(allSegmentsOnLabelmap.flat())),
metadata,
labelmaps2D,
}; };
}
const allSegmentsOnLabelmap = labelmaps2D.map(labelmap => labelmap.segmentsOnLabelmap);
const labelmap3D = {
segmentsOnLabelmap: Array.from(new Set(allSegmentsOnLabelmap.flat())),
metadata: [],
labelmaps2D,
}; };
// Segment metadata (shared across all layers).
const segmentationInOHIF = segmentationService.getSegmentation(segmentationId); const segmentationInOHIF = segmentationService.getSegmentation(segmentationId);
const representations = segmentationService.getRepresentationsForSegmentation(segmentationId); const representations = segmentationService.getRepresentationsForSegmentation(segmentationId);
const metadata = [];
Object.entries(segmentationInOHIF.segments).forEach(([segmentIndex, segment]) => { Object.entries(segmentationInOHIF.segments).forEach(([segmentIndex, segment]) => {
// segmentation service already has a color for each segment // segmentation service already has a color for each segment
@ -152,7 +191,7 @@ const commandsModule = ({
color.slice(0, 3).map(value => value / 255) color.slice(0, 3).map(value => value / 255)
).map(value => Math.round(value)); ).map(value => Math.round(value));
const segmentMetadata = { metadata[segmentIndex] = {
SegmentNumber: segmentIndex.toString(), SegmentNumber: segmentIndex.toString(),
SegmentLabel: label, SegmentLabel: label,
SegmentAlgorithmType: segment?.algorithmType || 'MANUAL', SegmentAlgorithmType: segment?.algorithmType || 'MANUAL',
@ -169,13 +208,76 @@ const commandsModule = ({
CodeMeaning: 'Tissue', 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;
// The referenced source images must be fully loaded (in cache) before we can
// build the SEG dataset against them; fail loudly rather than passing undefined
// frames to the adapter.
const resolveReferencedImage = (referencedImageId: string, sliceIndex: number) => {
const referencedImage = cache.getImage(referencedImageId);
if (!referencedImage) {
throw new Error(
`Referenced source image not in cache for segmentation slice ${sliceIndex} ` +
`(referencedImageId: ${referencedImageId}). Ensure the referenced series is fully loaded before storing.`
);
}
return referencedImage;
};
let referencedImages;
let labelmaps3D;
if (layers && layers.length > 1) {
const referencedImageIds =
layers[0].referencedImageIds ?? labelmapData.referencedImageIds ?? [];
referencedImages = referencedImageIds.map(resolveReferencedImage);
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, sliceIndex) =>
resolveReferencedImage(image.referencedImageId, sliceIndex)
);
labelmaps3D = buildLabelmap3D(imageIds, metadata);
}
const saveOptions = {
predecessorImageId, predecessorImageId,
...options, ...getSegmentationSaveOptions(customizationService, dataSourceStoreOverride),
}); ...generateOptions,
};
// A LABELMAP SEG frame stores a single label per voxel, so the labelmap
// encoder cannot represent overlapping segments — it keeps only the last
// layer written to each voxel. Overlapping segmentations arrive here as
// multiple layers, so switch those to the binary SEG encoding, which
// writes overlapping segments as separate frames referencing the same
// source slice.
const hasOverlappingLayers = Boolean(layers && layers.length > 1);
if (hasOverlappingLayers && saveOptions.sopClassUID === LABELMAP_SEG_SOP_CLASS_UID) {
console.warn(
'generateSegmentation: overlapping segments cannot be stored as a LABELMAP SEG; ' +
'switching to the binary SEG encoding for this store.'
);
saveOptions.sopClassUID = BITMAP_SEG_SOP_CLASS_UID;
}
const generatedSegmentation = generateSegmentation(
referencedImages,
labelmaps3D,
metaData,
saveOptions
);
return generatedSegmentation; return generatedSegmentation;
}, },
@ -194,8 +296,11 @@ const commandsModule = ({
const generatedSegmentation = actions.generateSegmentation({ const generatedSegmentation = actions.generateSegmentation({
segmentationId, segmentationId,
}); });
const storeFn = commandsManager.runCommand('createStoreFunction', {
downloadDICOMData(generatedSegmentation.dataset, `${segmentationInOHIF.label}`); dataSource: 'download',
defaultFileName: `${segmentationInOHIF.label}.dcm`,
});
storeFn(generatedSegmentation.dataset);
}, },
/** /**
* Stores a segmentation based on the provided segmentationId into a specified data source. * Stores a segmentation based on the provided segmentationId into a specified data source.
@ -217,11 +322,10 @@ const commandsModule = ({
} }
const { label, predecessorImageId } = segmentation; const { label, predecessorImageId } = segmentation;
const defaultDataSource = dataSource ?? extensionManager.getActiveDataSource()[0];
const { const {
value: reportName, value: reportName,
dataSourceName: selectedDataSource, dataSourceName,
series, series,
priorSeriesNumber, priorSeriesNumber,
action, action,
@ -231,55 +335,65 @@ const commandsModule = ({
predecessorImageId, predecessorImageId,
title: 'Store Segmentation', title: 'Store Segmentation',
modality, modality,
enableDownload: true,
}); });
if (action === PROMPT_RESPONSES.CREATE_REPORT) { if (action !== PROMPT_RESPONSES.CREATE_REPORT) {
try { return;
const selectedDataSourceConfig = selectedDataSource }
? extensionManager.getDataSources(selectedDataSource)[0]
: defaultDataSource;
const args = { const defaultFileName =
segmentationId, modality === 'RTSTRUCT' ? `rtss-${segmentationId}.dcm` : `${label || 'segmentation'}.dcm`;
options: {
SeriesDescription: series ? undefined : reportName || label || 'Contour Series',
SeriesNumber: series ? undefined : 1 + priorSeriesNumber,
predecessorImageId: series,
},
};
const generatedDataAsync =
(modality === 'SEG' && actions.generateSegmentation(args)) ||
(modality === 'RTSTRUCT' && actions.generateContour(args));
const generatedData = await generatedDataAsync;
if (!generatedData || !generatedData.dataset) { const storeFn = commandsManager.runCommand('createStoreFunction', {
throw new Error('Error during segmentation generation'); dataSource: dataSourceName,
} defaultFileName,
});
const { dataset: naturalizedReport } = generatedData; if (!storeFn) {
throw new Error(`No valid store for dataSource: ${dataSourceName}`);
}
// DCMJS assigns a dummy study id during creation, and this can cause problems, so clearing it out try {
if (naturalizedReport.StudyID === 'No Study ID') { const args = {
naturalizedReport.StudyID = ''; segmentationId,
} options: {
// Resolve store overrides against the data source we are storing into.
dataSource: dataSourceName,
SeriesDescription: series ? undefined : reportName || label || 'Contour Series',
SeriesNumber: series ? undefined : 1 + priorSeriesNumber,
predecessorImageId: series,
},
};
const generatedDataAsync =
(modality === 'SEG' && actions.generateSegmentation(args)) ||
(modality === 'RTSTRUCT' && actions.generateContour(args));
const generatedData = await generatedDataAsync;
await selectedDataSourceConfig.store.dicom(naturalizedReport); if (!generatedData?.dataset) {
throw new Error('Error during segmentation generation');
// add the information for where we stored it to the instance as well
naturalizedReport.wadoRoot = selectedDataSourceConfig.getConfig().wadoRoot;
DicomMetadataStore.addInstances([naturalizedReport], true);
return naturalizedReport;
} catch (error) {
console.debug('Error storing segmentation:', error);
throw error;
} }
const { dataset: naturalizedReport } = generatedData;
// DCMJS assigns a dummy study id during creation, and this can cause problems, so clearing it out
if (naturalizedReport.StudyID === 'No Study ID') {
naturalizedReport.StudyID = '';
}
await storeFn(naturalizedReport, {});
return naturalizedReport;
} catch (error) {
console.debug('Error storing segmentation:', error);
throw error;
} }
}, },
generateContour: async args => { generateContour: async args => {
const { segmentationId, options } = args; const { segmentationId, options } = args;
// `dataSource` is only used by the SEG store path; keep it out of the RTSS options.
const { dataSource: _dataSource, ...contourOptions } = options ?? {};
const segmentations = segmentationService.getSegmentation(segmentationId); const segmentations = segmentationService.getSegmentation(segmentationId);
// inject colors to the segmentIndex // inject colors to the segmentIndex
@ -292,10 +406,11 @@ const commandsModule = ({
Number(segmentIndex) Number(segmentIndex)
); );
}); });
const predecessorImageId = options?.predecessorImageId ?? segmentations.predecessorImageId; const predecessorImageId =
contourOptions.predecessorImageId ?? segmentations.predecessorImageId;
const dataset = await generateRTSSFromRepresentation(segmentations, { const dataset = await generateRTSSFromRepresentation(segmentations, {
predecessorImageId, predecessorImageId,
...options, ...contourOptions,
}); });
return { dataset }; return { dataset };
}, },
@ -307,14 +422,11 @@ const commandsModule = ({
downloadRTSS: async args => { downloadRTSS: async args => {
const { dataset } = await actions.generateContour(args); const { dataset } = await actions.generateContour(args);
const { InstanceNumber: instanceNumber = 1, SeriesInstanceUID: seriesUID } = dataset; const { InstanceNumber: instanceNumber = 1, SeriesInstanceUID: seriesUID } = dataset;
const storeFn = commandsManager.runCommand('createStoreFunction', {
try { dataSource: 'download',
//Create a URL for the binary. defaultFileName: `rtss-${seriesUID}-${instanceNumber}.dcm`,
const filename = `rtss-${seriesUID}-${instanceNumber}.dcm`; });
downloadDICOMData(dataset, filename); await storeFn(dataset);
} catch (e) {
console.warn(e);
}
}, },
toggleActiveSegmentationUtility: ({ itemId: buttonId }) => { toggleActiveSegmentationUtility: ({ itemId: buttonId }) => {

View File

@ -70,7 +70,10 @@ function SegmentSelector({
onValueChange={onValueChange} onValueChange={onValueChange}
value={value} value={value}
> >
<SelectTrigger className="overflow-hidden"> <SelectTrigger
className="overflow-hidden"
data-cy={`logical-contour-segment-${label.toLowerCase()}-trigger`}
>
<SelectValue placeholder={t(placeholder)} /> <SelectValue placeholder={t(placeholder)} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -180,6 +183,7 @@ function LogicalContourOperationOptions() {
value={value} value={value}
key={`logical-contour-operation-${value}`} key={`logical-contour-operation-${value}`}
onClick={() => setOperation(option)} onClick={() => setOperation(option)}
data-cy={`logical-contour-operation-${value}`}
> >
<Icons.ByName name={icon}></Icons.ByName> <Icons.ByName name={icon}></Icons.ByName>
</TabsTrigger> </TabsTrigger>
@ -207,6 +211,7 @@ function LogicalContourOperationOptions() {
/> />
<div className="flex justify-end pl-[34px]"> <div className="flex justify-end pl-[34px]">
<Button <Button
data-cy="apply-logical-contour-operation"
className="border-primary/60 grow border" className="border-primary/60 grow border"
variant="ghost" variant="ghost"
onClick={() => { onClick={() => {
@ -221,6 +226,7 @@ function LogicalContourOperationOptions() {
<div className="flex items-center justify-start gap-2"> <div className="flex items-center justify-start gap-2">
<Switch <Switch
id="logical-contour-operations-create-new-segment-switch" id="logical-contour-operations-create-new-segment-switch"
data-cy="logical-contour-create-new-segment-switch"
onCheckedChange={setCreateNewSegment} onCheckedChange={setCreateNewSegment}
></Switch> ></Switch>
<Label htmlFor="logical-contour-operations-create-new-segment-switch"> <Label htmlFor="logical-contour-operations-create-new-segment-switch">

View File

@ -0,0 +1,29 @@
import {
DEFAULT_SEG_STORE_MODE,
DEFAULT_SEG_STORE_TRANSFER_SYNTAX_UID,
type SegmentationMode,
} from '../utils/segmentationConfig';
export type SegmentLabelCustomization = {
enabledByDefault?: boolean;
labelColor?: number[];
hoverTimeout?: number;
background?: string;
};
export type SegmentationStoreCustomization = {
defaultMode?: SegmentationMode;
transferSyntaxUID?: string;
};
/** Extension-registered defaults: Label Map SEG + RLE Lossless. */
const segmentationCustomization = {
'segmentation.store.defaultMode': DEFAULT_SEG_STORE_MODE,
'segmentation.store.transferSyntaxUID': DEFAULT_SEG_STORE_TRANSFER_SYNTAX_UID,
'segmentation.segmentLabel': {
enabledByDefault: false,
hoverTimeout: 1,
} satisfies SegmentLabelCustomization,
};
export default segmentationCustomization;

View File

@ -0,0 +1,10 @@
import segmentationCustomization from './customizations/segmentationCustomization';
export default function getCustomizationModule() {
return [
{
name: 'default',
value: segmentationCustomization,
},
];
}

View File

@ -1,16 +1,237 @@
import { utils, Types as OhifTypes } from '@ohif/core'; import { utils, Types as OhifTypes, DicomMetadataStore, classes, log } from '@ohif/core';
import i18n from '@ohif/i18n'; import i18n from '@ohif/i18n';
import { metaData, eventTarget } from '@cornerstonejs/core'; import { metaData, eventTarget, utilities as csUtils } from '@cornerstonejs/core';
import { CONSTANTS, segmentation as cstSegmentation } from '@cornerstonejs/tools'; import { CONSTANTS, segmentation as cstSegmentation } from '@cornerstonejs/tools';
import { adaptersSEG, Enums } from '@cornerstonejs/adapters'; import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
import { SOPClassHandlerId } from './id'; import { SOPClassHandlerId } from './id';
import { dicomlabToRGB } from './utils/dicomlabToRGB'; import { dicomlabToRGB } from './utils/dicomlabToRGB';
import { getSegmentationParserType } from './utils/segmentationConfig';
import {
getFrameIndexFromImageId,
isLocalSchemeImageId,
stripFrameFromImageId,
} from './utils/segLocalImageIds';
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4', '1.2.840.10008.5.1.4.1.1.66.7']; const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4', '1.2.840.10008.5.1.4.1.1.66.7'];
const LABELMAP_SEG_SOP_CLASS_UID = '1.2.840.10008.5.1.4.1.1.66.7';
const loadPromises = {}; const loadPromises = {};
const SEG_LOAD_LOG_PREFIX = '[SEG load]';
// Max number of SEG frames fetched/decoded concurrently by the segmentation
// loader. Hard-coded to 16 for now; intended to become configurable (and to
// pair with the full-instance prefetch capability) in a follow-up.
const SEG_FRAME_DECODE_CONCURRENCY = 16;
function _normalizeImageId(imageId: string | string[] | undefined): string | undefined {
if (imageId == null) {
return undefined;
}
return Array.isArray(imageId) ? imageId[0] : imageId;
}
/**
* Expands a WADO-RS frame imageId (/frames/1) into one imageId per frame.
* Multiframe SEG is stored as separate /frames/N resources on the server.
*/
function getFrameImageIds(segImageId: string, numberOfFrames: number): string[] {
const frameMatch = segImageId.match(/(.*\/frames\/)(\d+)(.*)$/);
if (!frameMatch || numberOfFrames <= 1) {
return [segImageId];
}
const prefix = frameMatch[1];
const suffix = frameMatch[3] || '';
const frameImageIds: string[] = [];
for (let frameNumber = 1; frameNumber <= numberOfFrames; frameNumber++) {
frameImageIds.push(`${prefix}${frameNumber}${suffix}`);
}
return frameImageIds;
}
function _getSegNumberOfFrames(instance: Record<string, unknown>): number {
const fromTag = Number(instance.NumberOfFrames);
if (fromTag > 0) {
return fromTag;
}
const perFrame = instance.PerFrameFunctionalGroupsSequence;
if (Array.isArray(perFrame) && perFrame.length > 0) {
return perFrame.length;
}
return 1;
}
function _ensureSegImageIdMetadataRegistered(
imageId: string | undefined,
instance: Record<string, unknown>
): void {
if (!imageId) {
return;
}
const metadataProvider = classes.MetadataProvider;
const StudyInstanceUID = instance.StudyInstanceUID as string | undefined;
const SeriesInstanceUID = instance.SeriesInstanceUID as string | undefined;
const SOPInstanceUID = (instance.SOPInstanceUID || instance.SopInstanceUID) as
| string
| undefined;
if (!StudyInstanceUID || !SeriesInstanceUID || !SOPInstanceUID) {
return;
}
metadataProvider.addImageIdToUIDs(imageId, {
StudyInstanceUID,
SeriesInstanceUID,
SOPInstanceUID,
frameNumber: getFrameIndexFromImageId(imageId),
});
}
/** Ensures metadataProvider.get('instance', imageId) resolves for frame-qualified local SEG ids. */
function _ensureSegInstanceMetadataAvailable(
imageId: string | undefined,
instance: Record<string, unknown>
): void {
if (!imageId) {
return;
}
_ensureSegImageIdMetadataRegistered(imageId, instance);
if (metaData.get('instance', imageId)) {
return;
}
const StudyInstanceUID = instance.StudyInstanceUID as string | undefined;
const SeriesInstanceUID = instance.SeriesInstanceUID as string | undefined;
const SOPInstanceUID = (instance.SOPInstanceUID || instance.SopInstanceUID) as
| string
| undefined;
const storedInstance =
StudyInstanceUID && SeriesInstanceUID && SOPInstanceUID
? DicomMetadataStore.getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID)
: undefined;
classes.MetadataProvider.addCustomMetadata(
imageId,
'instance',
storedInstance || instance
);
}
function _getSegDataSource(extensionManager, instance: Record<string, unknown>) {
const StudyInstanceUID = instance.StudyInstanceUID as string | undefined;
const SeriesInstanceUID = instance.SeriesInstanceUID as string | undefined;
const SOPInstanceUID = (instance.SOPInstanceUID || instance.SopInstanceUID) as
| string
| undefined;
let localUrl: string | undefined;
if (StudyInstanceUID && SeriesInstanceUID && SOPInstanceUID) {
const storedInstance = DicomMetadataStore.getInstance(
StudyInstanceUID,
SeriesInstanceUID,
SOPInstanceUID
);
localUrl = storedInstance?.url as string | undefined;
}
localUrl = localUrl || (instance.url as string | undefined);
if (localUrl && isLocalSchemeImageId(localUrl)) {
const dicomLocal = extensionManager.getDataSources('dicomlocal');
if (dicomLocal?.[0]) {
return dicomLocal[0];
}
}
return extensionManager.getActiveDataSource()[0];
}
function _getSegImageIdFromInstance(
instance: Record<string, unknown>,
dataSource: { getImageIdsForInstance?: (args: { instance: unknown; frame?: number }) => unknown }
): string | undefined {
const numberOfFrames = _getSegNumberOfFrames(instance);
const frame = numberOfFrames > 1 ? 1 : undefined;
return _normalizeImageId(
dataSource.getImageIdsForInstance?.({ instance, frame }) as string | string[] | undefined
);
}
function _resolveFrameImageIds(
segImageIdStr: string,
instance: Record<string, unknown>,
dataSource: { getImageIdsForInstance?: (args: { instance: unknown; frame?: number }) => unknown }
): string[] {
const numberOfFrames = _getSegNumberOfFrames(instance);
const fromFrameUrl = getFrameImageIds(segImageIdStr, numberOfFrames);
if (fromFrameUrl.length > 1) {
return fromFrameUrl;
}
if (numberOfFrames <= 1) {
return [segImageIdStr];
}
const frameImageIds: string[] = [];
for (let frame = 1; frame <= numberOfFrames; frame++) {
const frameImageId = _normalizeImageId(
dataSource.getImageIdsForInstance?.({ instance, frame }) as string | string[] | undefined
);
if (frameImageId) {
frameImageIds.push(frameImageId);
}
}
return frameImageIds.length ? frameImageIds : [segImageIdStr];
}
function _logSegImageIds({
segDisplaySet,
segImageIdStr,
frameImageIds,
referencedImageIds,
}: {
segDisplaySet: AppTypes.DisplaySet;
segImageIdStr: string;
frameImageIds: string[];
referencedImageIds: string[];
}) {
const instance = segDisplaySet.instance as Record<string, unknown>;
const numberOfFrames = Number(instance?.NumberOfFrames) || 1;
log.debug(SEG_LOAD_LOG_PREFIX, 'Loading SEG pixel data', {
SOPInstanceUID: segDisplaySet.SOPInstanceUID,
SeriesInstanceUID: segDisplaySet.SeriesInstanceUID,
SOPClassUID: segDisplaySet.SOPClassUID,
NumberOfFrames: numberOfFrames,
segmentCount: Object.keys(segDisplaySet.segments || {}).length,
referencedDisplaySetInstanceUID: segDisplaySet.referencedDisplaySetInstanceUID,
referencedImageIdCount: referencedImageIds.length,
referencedImageIds,
segImageIdForMetadata: segImageIdStr,
frameImageIds,
loadSegFramesIndividually: frameImageIds.length > 1,
});
}
function _getDisplaySetsFromSeries( function _getDisplaySetsFromSeries(
instances, instances,
servicesManager: AppTypes.ServicesManager, servicesManager: AppTypes.ServicesManager,
@ -30,6 +251,7 @@ function _getDisplaySetsFromSeries(
SeriesDate, SeriesDate,
StructureSetDate, StructureSetDate,
SOPClassUID, SOPClassUID,
FrameOfReferenceUID,
wadoRoot, wadoRoot,
wadoUri, wadoUri,
wadoUriRoot, wadoUriRoot,
@ -49,6 +271,7 @@ function _getDisplaySetsFromSeries(
StudyInstanceUID, StudyInstanceUID,
SOPClassHandlerId, SOPClassHandlerId,
SOPClassUID, SOPClassUID,
FrameOfReferenceUID,
referencedImages: null, referencedImages: null,
referencedSeriesInstanceUID: null, referencedSeriesInstanceUID: null,
referencedDisplaySetInstanceUID: null, referencedDisplaySetInstanceUID: null,
@ -104,6 +327,7 @@ function _getDisplaySetsFromSeries(
if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) { if (addedDisplaySet.SeriesInstanceUID === displaySet.referencedSeriesInstanceUID) {
displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID; displaySet.referencedDisplaySetInstanceUID = addedDisplaySet.displaySetInstanceUID;
displaySet.isReconstructable = addedDisplaySet.isReconstructable; displaySet.isReconstructable = addedDisplaySet.isReconstructable;
displaySet.FrameOfReferenceUID = addedDisplaySet.FrameOfReferenceUID;
unsubscribe(); unsubscribe();
} }
} }
@ -111,6 +335,7 @@ function _getDisplaySetsFromSeries(
} else { } else {
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID; displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
displaySet.isReconstructable = referencedDisplaySet.isReconstructable; displaySet.isReconstructable = referencedDisplaySet.isReconstructable;
displaySet.FrameOfReferenceUID = referencedDisplaySet.FrameOfReferenceUID;
} }
displaySet.load = async ({ headers }) => displaySet.load = async ({ headers }) =>
@ -167,6 +392,11 @@ function _load(
}); });
}); });
// Expose the in-flight load promise so observers (e.g. the viewport service
// waiting to attach the representation) can react to a load failure without
// re-invoking load().
segDisplaySet.loadingPromise = loadPromises[SOPInstanceUID];
return loadPromises[SOPInstanceUID]; return loadPromises[SOPInstanceUID];
} }
@ -174,16 +404,18 @@ async function _loadSegments({
extensionManager, extensionManager,
servicesManager, servicesManager,
segDisplaySet, segDisplaySet,
headers, }: withAppTypes<{ segDisplaySet: AppTypes.DisplaySet }>) {
}: withAppTypes) { const { segmentationService, uiNotificationService, customizationService } =
const utilityModule = extensionManager.getModuleEntry( servicesManager.services;
'@ohif/extension-cornerstone.utilityModule.common' const instance = segDisplaySet.instance as Record<string, unknown>;
); const dataSource = _getSegDataSource(extensionManager, instance);
const segImageIdStr = _getSegImageIdFromInstance(instance, dataSource);
const { segmentationService, uiNotificationService } = servicesManager.services; if (!segImageIdStr) {
throw new Error(
const { dicomLoaderService } = utilityModule.exports; 'Could not get imageId for SEG instance (no local wadouri url and getImageIdsForInstance returned nothing).'
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers); );
}
const referencedDisplaySet = servicesManager.services.displaySetService.getDisplaySetByUID( const referencedDisplaySet = servicesManager.services.displaySetService.getDisplaySetByUID(
segDisplaySet.referencedDisplaySetInstanceUID segDisplaySet.referencedDisplaySetInstanceUID
@ -193,31 +425,114 @@ async function _loadSegments({
throw new Error('referencedDisplaySet is missing for SEG'); throw new Error('referencedDisplaySet is missing for SEG');
} }
// Prefer cached stack imageIds (multiframe SEG fix #4890), then data source expansion.
let { imageIds } = referencedDisplaySet; let { imageIds } = referencedDisplaySet;
if (!imageIds) { if (!imageIds?.length) {
// try images imageIds = dataSource.getImageIdsForDisplaySet?.(referencedDisplaySet);
const { images } = referencedDisplaySet;
imageIds = images.map(image => image.imageId);
} }
// Todo: what should be defaults here if (!imageIds?.length) {
imageIds = (referencedDisplaySet as { images?: { imageId: string }[] }).images?.map(
(img: { imageId: string }) => img.imageId
);
}
if (!imageIds?.length) {
throw new Error('referencedDisplaySet has no imageIds');
}
(segDisplaySet as AppTypes.DisplaySet & { referencedImageIds?: string[] }).referencedImageIds =
imageIds;
if (!referencedDisplaySet.imageIds?.length) {
referencedDisplaySet.imageIds = imageIds;
}
const frameImageIds = _resolveFrameImageIds(
segImageIdStr,
segDisplaySet.instance as Record<string, unknown>,
dataSource
);
const segImageIdForMetadata = isLocalSchemeImageId(segImageIdStr)
? stripFrameFromImageId(segImageIdStr)
: segImageIdStr;
_logSegImageIds({
segDisplaySet,
segImageIdStr: segImageIdForMetadata,
frameImageIds,
referencedImageIds: imageIds,
});
_ensureSegInstanceMetadataAvailable(segImageIdForMetadata, instance);
frameImageIds.forEach(id => _ensureSegInstanceMetadataAvailable(id, instance));
const tolerance = 0.001; const tolerance = 0.001;
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => { const onProgress = evt => {
const { percentComplete } = evt.detail; const { percentComplete } = evt.detail;
segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, { segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, {
percentComplete, percentComplete,
}); });
}); };
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, onProgress);
const results = await adaptersSEG.Cornerstone3D.Segmentation.createFromDICOMSegBuffer( // Fetch the whole SEG instance as a single Part 10 object and register its
imageIds, // per-frame compressed pixels into the Cornerstone3D frame registry, so the
arrayBuffer, // per-frame loads below are served locally instead of one network request
{ metadataProvider: metaData, tolerance } // per frame: SEG frames are so small and numerous that one bulk fetch beats
); // hundreds of tiny requests. Enabled by default; per-frame loading is the
// exception (loadMultiframeAsPart10: false in the data source config, or the
// cornerstone.segmentation.loadMultiframeAsPart10 customization). The
// prefetch is awaited until it completes OR fails — deliberately no timeout:
// a failed/unsupported instance fetch resolves quickly and falls back to
// per-frame, while a slow large fetch is still the fastest way to all frames.
const loadMultiframeAsPart10 =
(dataSource?.getConfig?.()?.loadMultiframeAsPart10 as boolean | undefined) ??
(customizationService?.getCustomization?.(
'cornerstone.segmentation.loadMultiframeAsPart10'
) as boolean | undefined) ??
true;
let prefetch;
if (loadMultiframeAsPart10) {
prefetch = dataSource.retrieve?.prefetchInstanceFrames?.({
instance,
imageId: segImageIdForMetadata,
});
if (prefetch?.done) {
await prefetch.done;
}
}
let results;
try {
results = await adaptersSEG.Cornerstone3D.Segmentation.createFromDicomSegImageId(
imageIds,
segImageIdForMetadata,
{
metadataProvider: metaData,
tolerance,
parserType: getSegmentationParserType(
segDisplaySet.SOPClassUID,
customizationService
),
frameImageIds,
concurrency: SEG_FRAME_DECODE_CONCURRENCY,
}
);
} finally {
eventTarget.removeEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, onProgress);
prefetch?.cancel?.();
}
let usedRecommendedDisplayCIELabValue = true; let usedRecommendedDisplayCIELabValue = true;
results.segMetadata.data.forEach((data, i) => { const resultsTyped = results as {
segMetadata: { data: { rgba?: number[]; RecommendedDisplayCIELabValue?: number[] }[] };
};
resultsTyped.segMetadata.data.forEach((data, i) => {
if (i > 0) { if (i > 0) {
data.rgba = data.RecommendedDisplayCIELabValue; data.rgba = data.RecommendedDisplayCIELabValue;
@ -242,6 +557,17 @@ async function _loadSegments({
} }
Object.assign(segDisplaySet, results); Object.assign(segDisplaySet, results);
const labelMapImageIds = (results as { labelMapImages?: { imageId: string }[][] })
.labelMapImages?.flat()
.map(image => image.imageId);
log.debug(SEG_LOAD_LOG_PREFIX, 'SEG parse complete', {
SOPInstanceUID: segDisplaySet.SOPInstanceUID,
labelMapImageCount: labelMapImageIds?.length ?? 0,
labelMapImageIds,
segmentIndices: Object.keys(segDisplaySet.segments || {}),
});
} }
function _segmentationExists(segDisplaySet) { function _segmentationExists(segDisplaySet) {

View File

@ -4,6 +4,7 @@ import React from 'react';
import getSopClassHandlerModule from './getSopClassHandlerModule'; import getSopClassHandlerModule from './getSopClassHandlerModule';
import getHangingProtocolModule from './getHangingProtocolModule'; import getHangingProtocolModule from './getHangingProtocolModule';
import getCommandsModule from './commandsModule'; import getCommandsModule from './commandsModule';
import getCustomizationModule from './getCustomizationModule';
import { getToolbarModule } from './getToolbarModule'; import { getToolbarModule } from './getToolbarModule';
const Component = React.lazy(() => { const Component = React.lazy(() => {
@ -28,6 +29,7 @@ const extension = {
*/ */
id, id,
getCommandsModule, getCommandsModule,
getCustomizationModule,
getToolbarModule, getToolbarModule,
getViewportModule({ servicesManager, extensionManager, commandsManager }) { getViewportModule({ servicesManager, extensionManager, commandsManager }) {
const ExtendedOHIFCornerstoneSEGViewport = props => { const ExtendedOHIFCornerstoneSEGViewport = props => {

View File

@ -0,0 +1,32 @@
export function isLocalSchemeImageId(imageId: string): boolean {
return /^(wadouri:|dicomfile:|dicomweb:)/.test(imageId);
}
export function stripFrameFromImageId(imageId: string): string {
const queryIndex = imageId.indexOf('?');
if (queryIndex === -1) {
return imageId;
}
const basePath = imageId.slice(0, queryIndex);
const rebuiltQuery = imageId
.slice(queryIndex + 1)
.split('&')
.filter(param => param.split('=')[0] !== 'frame')
.join('&');
return rebuiltQuery ? `${basePath}?${rebuiltQuery}` : basePath;
}
export function appendFrameToImageId(baseImageId: string, frame: number): string {
const withoutFrame = stripFrameFromImageId(baseImageId);
const separator = withoutFrame.includes('?') ? '&' : '?';
return `${withoutFrame}${separator}frame=${frame}`;
}
export function getFrameIndexFromImageId(imageId: string): number {
const frameMatch = imageId.match(/(?:&|\?)frame=(\d+)/);
return frameMatch ? Number(frameMatch[1]) : 1;
}

View File

@ -0,0 +1,100 @@
export const LABELMAP_SEG_SOP_CLASS_UID = '1.2.840.10008.5.1.4.1.1.66.7';
export const BITMAP_SEG_SOP_CLASS_UID = '1.2.840.10008.5.1.4.1.1.66.4';
/** RLE Lossless — OHIF default SEG store transfer syntax. */
export const DEFAULT_SEG_STORE_TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.5';
/** OHIF default SEG store mode (Label Map Segmentation SOP Class). */
export const DEFAULT_SEG_STORE_MODE = 'labelmap' as const;
export type SegmentationMode = 'labelmap' | 'bitmap';
/**
* Per-data-source overrides for the SEG store defaults. A data source may set
* these under `configuration.segmentation.store` to override the values coming
* from the `segmentation.store.*` customizations. Different back ends support
* different SEG encodings, so the data source is allowed to win over the
* app-wide customization default.
*/
export type SegmentationStoreOverride = {
defaultMode?: SegmentationMode;
transferSyntaxUID?: string;
};
type SegmentationCustomizationReader = {
getCustomization: (customizationId: string) => unknown;
};
function getStoreDefaultMode(
customizationService?: SegmentationCustomizationReader,
override?: SegmentationStoreOverride
): SegmentationMode {
const mode =
override?.defaultMode ??
(customizationService?.getCustomization('segmentation.store.defaultMode') as
| SegmentationMode
| undefined) ??
DEFAULT_SEG_STORE_MODE;
return mode === 'bitmap' ? 'bitmap' : 'labelmap';
}
function getStoreTransferSyntaxUID(
customizationService?: SegmentationCustomizationReader,
override?: SegmentationStoreOverride
): string {
return (
override?.transferSyntaxUID ??
(customizationService?.getCustomization(
'segmentation.store.transferSyntaxUID'
) as string | undefined) ??
DEFAULT_SEG_STORE_TRANSFER_SYNTAX_UID
);
}
/**
* Resolves the parser type for loading a DICOM SEG instance.
* Uses the instance SOP Class UID when it is a known SEG class; otherwise falls back to store defaultMode.
*/
export function getSegmentationParserType(
sopClassUID: string | undefined,
customizationService?: SegmentationCustomizationReader
): SegmentationMode {
if (sopClassUID === LABELMAP_SEG_SOP_CLASS_UID) {
return 'labelmap';
}
if (sopClassUID === BITMAP_SEG_SOP_CLASS_UID) {
return 'bitmap';
}
return getStoreDefaultMode(customizationService);
}
/**
* Options passed to @cornerstonejs/adapters generateSegmentation when exporting or storing SEG.
*
* Defaults to **Label Map + RLE Lossless**. Customizations (or per-data-source
* `configuration.segmentation.store`) are only needed to opt into bitmap and/or
* uncompressed Explicit VR Little Endian.
*/
export function getSegmentationSaveOptions(
customizationService?: SegmentationCustomizationReader,
override?: SegmentationStoreOverride
): {
sopClassUID: string;
transferSyntaxUID: string;
transferSyntaxUid: string;
} {
const defaultMode = getStoreDefaultMode(customizationService, override);
const sopClassUID =
defaultMode === 'bitmap' ? BITMAP_SEG_SOP_CLASS_UID : LABELMAP_SEG_SOP_CLASS_UID;
const transferSyntaxUID = getStoreTransferSyntaxUID(
customizationService,
override
);
return {
sopClassUID,
transferSyntaxUID,
transferSyntaxUid: transferSyntaxUID,
};
}

View File

@ -95,10 +95,13 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
}; };
const getCornerstoneViewport = useCallback(() => { const getCornerstoneViewport = useCallback(() => {
// Stack uses the referenced series (data[0]); SEG is applied as an overlay (data[1]).
// Passing only the SEG display set leaves the stack with derived labelmap imageIds,
// which are not displayable without the underlying grayscale series.
return ( return (
<OHIFCornerstoneViewport <OHIFCornerstoneViewport
{...props} {...props}
displaySets={[segDisplaySet]} displaySets={[referencedDisplaySet, segDisplaySet]}
viewportOptions={{ viewportOptions={{
viewportType: viewportOptions.viewportType, viewportType: viewportOptions.viewportType,
toolGroupId: toolGroupId, toolGroupId: toolGroupId,
@ -111,7 +114,14 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) {
}} }}
/> />
); );
}, [viewportId, segDisplaySet, toolGroupId, props, viewportOptions]); }, [
viewportId,
segDisplaySet,
referencedDisplaySet,
toolGroupId,
props,
viewportOptions,
]);
useEffect(() => { useEffect(() => {
if (segIsLoading) { if (segIsLoading) {

View File

@ -1,8 +1,8 @@
const webpack = require('webpack'); const webpack = require('@rspack/core');
const { merge } = require('webpack-merge'); const { merge } = require('webpack-merge');
const path = require('path'); const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js'); const webpackCommon = require('./../../../.webpack/webpack.base.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = webpack.CssExtractRspackPlugin;
const pkg = require('./../package.json'); const pkg = require('./../package.json');
@ -36,9 +36,11 @@ module.exports = (env, argv) => {
sideEffects: true, sideEffects: true,
}, },
output: { output: {
library: {
name: 'ohif-extension-cornerstone-dicom-sr',
type: 'umd',
},
path: ROOT_DIR, path: ROOT_DIR,
library: 'ohif-extension-cornerstone-dicom-sr',
libraryTarget: 'umd',
filename: pkg.main, filename: pkg.main,
}, },
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],

View File

@ -3,6 +3,631 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.13.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.88...v3.13.0-beta.89) (2026-06-10)
### Bug Fixes
* ohif tests to run with cornerstone 3d 5.0 ([#6043](https://github.com/OHIF/Viewers/issues/6043)) ([6dd150d](https://github.com/OHIF/Viewers/commit/6dd150d401ad73d60632a23378b7dfd4b5142690))
# [3.13.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.87...v3.13.0-beta.88) (2026-06-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.86...v3.13.0-beta.87) (2026-05-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.85...v3.13.0-beta.86) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.84...v3.13.0-beta.85) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.83...v3.13.0-beta.84) (2026-05-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.82...v3.13.0-beta.83) (2026-05-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.81...v3.13.0-beta.82) (2026-05-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.80...v3.13.0-beta.81) (2026-05-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.79...v3.13.0-beta.80) (2026-05-20)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.78...v3.13.0-beta.79) (2026-05-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.77...v3.13.0-beta.78) (2026-05-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.76...v3.13.0-beta.77) (2026-05-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.75...v3.13.0-beta.76) (2026-05-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.74...v3.13.0-beta.75) (2026-05-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.73...v3.13.0-beta.74) (2026-05-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.72...v3.13.0-beta.73) (2026-05-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.71...v3.13.0-beta.72) (2026-05-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.70...v3.13.0-beta.71) (2026-05-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.69...v3.13.0-beta.70) (2026-05-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.68...v3.13.0-beta.69) (2026-05-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.67...v3.13.0-beta.68) (2026-05-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.66...v3.13.0-beta.67) (2026-04-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.65...v3.13.0-beta.66) (2026-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.64...v3.13.0-beta.65) (2026-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.63...v3.13.0-beta.64) (2026-04-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.62...v3.13.0-beta.63) (2026-04-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.61...v3.13.0-beta.62) (2026-04-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.60...v3.13.0-beta.61) (2026-04-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.59...v3.13.0-beta.60) (2026-04-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.58...v3.13.0-beta.59) (2026-04-16)
### Bug Fixes
* Use newer ONNX version and load without errors ([#5854](https://github.com/OHIF/Viewers/issues/5854)) ([cc5dc20](https://github.com/OHIF/Viewers/commit/cc5dc201649bcbfb4cfad21141dbd56000d30f53)), closes [#5875](https://github.com/OHIF/Viewers/issues/5875) [#5884](https://github.com/OHIF/Viewers/issues/5884) [#5886](https://github.com/OHIF/Viewers/issues/5886) [#5893](https://github.com/OHIF/Viewers/issues/5893) [#5874](https://github.com/OHIF/Viewers/issues/5874) [#5865](https://github.com/OHIF/Viewers/issues/5865)
# [3.13.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.57...v3.13.0-beta.58) (2026-04-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.56...v3.13.0-beta.57) (2026-04-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.55...v3.13.0-beta.56) (2026-04-10)
### Bug Fixes
* A couple of changes to enable cs3d integration build ([#5944](https://github.com/OHIF/Viewers/issues/5944)) ([f6bbd5c](https://github.com/OHIF/Viewers/commit/f6bbd5c779e4692dc47c93327a890661b8dcc174))
# [3.13.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.54...v3.13.0-beta.55) (2026-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.53...v3.13.0-beta.54) (2026-04-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.52...v3.13.0-beta.53) (2026-04-06)
### Reverts
* rename DisplaySet.frameOfReferenceUID back to FrameOfReferenceUID ([#5943](https://github.com/OHIF/Viewers/issues/5943)) ([0e933c2](https://github.com/OHIF/Viewers/commit/0e933c256e07b7cda35ed2ba3cfb1ab35d895d57))
# [3.13.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.51...v3.13.0-beta.52) (2026-04-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.50...v3.13.0-beta.51) (2026-04-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.49...v3.13.0-beta.50) (2026-04-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.48...v3.13.0-beta.49) (2026-04-02)
### Bug Fixes
* **segmentation:** restrict overlay segmentation menu to same frame of reference as viewport background display set ([#5900](https://github.com/OHIF/Viewers/issues/5900)) ([b9029ef](https://github.com/OHIF/Viewers/commit/b9029ef6f8d63a0e36ec929310c1c5ad3f563ef8))
# [3.13.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.47...v3.13.0-beta.48) (2026-04-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.46...v3.13.0-beta.47) (2026-03-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.45...v3.13.0-beta.46) (2026-03-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.44...v3.13.0-beta.45) (2026-03-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.43...v3.13.0-beta.44) (2026-03-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.42...v3.13.0-beta.43) (2026-03-17)
### Bug Fixes
* **SR:** Added support for spline and live wire SR items. ([#5870](https://github.com/OHIF/Viewers/issues/5870)) ([1d4802c](https://github.com/OHIF/Viewers/commit/1d4802c2a38dd75dca7afca87913e100453c9135))
# [3.13.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.41...v3.13.0-beta.42) (2026-03-17)
### Features
* Add combined build ([#5895](https://github.com/OHIF/Viewers/issues/5895)) ([1df671e](https://github.com/OHIF/Viewers/commit/1df671e9ab67d43f98bf09f6838ddc0a5f220277))
# [3.13.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.40...v3.13.0-beta.41) (2026-03-16)
### Bug Fixes
* **sr-hydration:** enable hydration and arrow navigation for 3D SR measurements ([#5887](https://github.com/OHIF/Viewers/issues/5887)) ([7a38903](https://github.com/OHIF/Viewers/commit/7a38903b1977b1cf0f3a0ba8a4a823fac5e8fdeb))
# [3.13.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.39...v3.13.0-beta.40) (2026-03-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.38...v3.13.0-beta.39) (2026-03-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.37...v3.13.0-beta.38) (2026-03-12)
### Bug Fixes
* **segmentation:** Display "No description S:{series number} {modality}" for segmentations with no label. ([#5874](https://github.com/OHIF/Viewers/issues/5874)) ([7b5d0ce](https://github.com/OHIF/Viewers/commit/7b5d0ce40e5422ccfce6091ac3d7559586cbe57e))
# [3.13.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.36...v3.13.0-beta.37) (2026-03-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.35...v3.13.0-beta.36) (2026-03-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.34...v3.13.0-beta.35) (2026-03-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.33...v3.13.0-beta.34) (2026-03-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.32...v3.13.0-beta.33) (2026-03-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.31...v3.13.0-beta.32) (2026-03-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.30...v3.13.0-beta.31) (2026-03-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.29...v3.13.0-beta.30) (2026-03-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.28...v3.13.0-beta.29) (2026-03-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.27...v3.13.0-beta.28) (2026-03-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.26...v3.13.0-beta.27) (2026-03-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.25...v3.13.0-beta.26) (2026-02-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.24...v3.13.0-beta.25) (2026-02-26)
### Bug Fixes
* Update CS3D 4.18.2 ([#5837](https://github.com/OHIF/Viewers/issues/5837)) ([5370b93](https://github.com/OHIF/Viewers/commit/5370b9382e7bfddeadd69bab9e653f5f997a6e19))
# [3.13.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.23...v3.13.0-beta.24) (2026-02-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.22...v3.13.0-beta.23) (2026-02-23)
### Bug Fixes
* palette color 8 encoded 16 ([#5823](https://github.com/OHIF/Viewers/issues/5823)) ([eeca1f7](https://github.com/OHIF/Viewers/commit/eeca1f7a7b529d1700f5d281a6fd98074547ffe9)), closes [#5813](https://github.com/OHIF/Viewers/issues/5813)
# [3.13.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.21...v3.13.0-beta.22) (2026-02-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.20...v3.13.0-beta.21) (2026-02-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.19...v3.13.0-beta.20) (2026-02-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.18...v3.13.0-beta.19) (2026-02-20)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.17...v3.13.0-beta.18) (2026-02-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.16...v3.13.0-beta.17) (2026-02-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.15...v3.13.0-beta.16) (2026-02-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
# [3.13.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.14...v3.13.0-beta.15) (2026-02-17) # [3.13.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.14...v3.13.0-beta.15) (2026-02-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr **Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr

View File

@ -1,6 +1,6 @@
{ {
"name": "@ohif/extension-cornerstone-dicom-sr", "name": "@ohif/extension-cornerstone-dicom-sr",
"version": "3.13.0-beta.15", "version": "3.13.0-beta.125",
"description": "OHIF extension for an SR Cornerstone Viewport", "description": "OHIF extension for an SR Cornerstone Viewport",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
@ -8,9 +8,7 @@
"main": "dist/ohif-extension-cornerstone-dicom-sr.umd.js", "main": "dist/ohif-extension-cornerstone-dicom-sr.umd.js",
"module": "src/index.tsx", "module": "src/index.tsx",
"engines": { "engines": {
"node": ">=14", "node": ">=24"
"npm": ">=6",
"yarn": ">=1.16.0"
}, },
"files": [ "files": [
"dist", "dist",
@ -19,36 +17,38 @@
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
"keywords": [
"ohif-extension"
],
"scripts": { "scripts": {
"clean": "shx rm -rf dist", "clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules", "clean:deep": "pnpm run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", "dev": "cross-env NODE_ENV=development rspack build --config .webpack/webpack.dev.js --watch",
"dev:cornerstone": "yarn run dev", "dev:cornerstone": "pnpm run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", "build": "cross-env NODE_ENV=production rspack build --config .webpack/webpack.prod.js",
"build:package-1": "yarn run build", "build:package-1": "pnpm run build",
"start": "yarn run dev", "start": "pnpm run dev",
"test:unit": "jest --watchAll", "test:unit": "jest --watchAll",
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.13.0-beta.15", "@ohif/core": "workspace:*",
"@ohif/extension-cornerstone": "3.13.0-beta.15", "@ohif/extension-cornerstone": "workspace:*",
"@ohif/extension-measurement-tracking": "3.13.0-beta.15", "@ohif/ui": "workspace:*",
"@ohif/ui": "3.13.0-beta.15", "dcmjs": "0.52.0",
"dcmjs": "0.49.4",
"dicom-parser": "1.8.21", "dicom-parser": "1.8.21",
"hammerjs": "2.0.8", "hammerjs": "2.0.8",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"react": "18.3.1" "react": "18.3.1"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "7.28.2", "@babel/runtime": "7.29.7",
"@cornerstonejs/adapters": "4.15.29", "@cornerstonejs/adapters": "5.4.17",
"@cornerstonejs/core": "4.15.29", "@cornerstonejs/core": "5.4.17",
"@cornerstonejs/tools": "4.15.29", "@cornerstonejs/tools": "5.4.17",
"classnames": "2.5.1" "classnames": "2.5.1"
} },
"devDependencies": {
"cross-env": "7.0.3"
},
"keywords": [
"ohif-extension"
]
} }

View File

@ -1,14 +1,11 @@
import { metaData } from '@cornerstonejs/core'; import { metaData } from '@cornerstonejs/core';
import OHIF, { DicomMetadataStore, utils } from '@ohif/core'; import OHIF from '@ohif/core';
import dcmjs from 'dcmjs';
import { adaptersSR } from '@cornerstonejs/adapters'; import { adaptersSR } from '@cornerstonejs/adapters';
import getFilteredCornerstoneToolState from './utils/getFilteredCornerstoneToolState'; import getFilteredCornerstoneToolState from './utils/getFilteredCornerstoneToolState';
import hydrateStructuredReport from './utils/hydrateStructuredReport'; import hydrateStructuredReport from './utils/hydrateStructuredReport';
const { downloadBlob } = utils;
const { MeasurementReport } = adaptersSR.Cornerstone3D; const { MeasurementReport } = adaptersSR.Cornerstone3D;
const { log } = OHIF; const { log } = OHIF;
@ -75,24 +72,8 @@ const commandsModule = (props: withAppTypes) => {
/** /**
* *
* @param measurementData An array of measurements from the measurements service * @param measurementData An array of measurements from the measurements service
* @param additionalFindingTypes toolTypes that should be stored with labels as Findings
* @param options Naturalized DICOM JSON headers to merge into the displaySet.
* as opposed to Finding Sites.
* that you wish to serialize. * that you wish to serialize.
*/ * @param dataSource The data source name ('download', 'copyToClipboard', or a named data source).
downloadReport: ({ measurementData, additionalFindingTypes, options = {} }) => {
const srDataset = _generateReport(measurementData, additionalFindingTypes, options);
const reportBlob = dcmjs.data.datasetToBlob(srDataset);
//Create a URL for the binary.
downloadBlob(reportBlob, { filename: 'dicom-sr.dcm' });
},
/**
*
* @param measurementData An array of measurements from the measurements service
* that you wish to serialize.
* @param dataSource The dataSource that you wish to use to persist the data.
* @param additionalFindingTypes toolTypes that should be stored with labels as Findings * @param additionalFindingTypes toolTypes that should be stored with labels as Findings
* @param options Naturalized DICOM JSON headers to merge into the displaySet. * @param options Naturalized DICOM JSON headers to merge into the displaySet.
* @return The naturalized report * @return The naturalized report
@ -103,23 +84,30 @@ const commandsModule = (props: withAppTypes) => {
additionalFindingTypes, additionalFindingTypes,
options = {}, options = {},
}) => { }) => {
// Use the @cornerstonejs adapter for converting to/from DICOM
// But it is good enough for now whilst we only have cornerstone as a datasource.
log.info('[DICOMSR] storeMeasurements'); log.info('[DICOMSR] storeMeasurements');
if (!dataSource || !dataSource.store || !dataSource.store.dicom) { const storeFn = commandsManager.runCommand('createStoreFunction', {
log.error('[DICOMSR] datasource has no dataSource.store.dicom endpoint!'); dataSource,
defaultFileName: 'dicom-sr.dcm',
});
if (!storeFn) {
log.error('[DICOMSR] No valid store for dataSource:', dataSource);
return Promise.reject({}); return Promise.reject({});
} }
try { try {
const naturalizedReport = _generateReport(measurementData, additionalFindingTypes, options); const naturalizedReport = _generateReport(
measurementData,
additionalFindingTypes,
options
);
const { StudyInstanceUID, ContentSequence } = naturalizedReport; const { ContentSequence } = naturalizedReport;
// The content sequence has 5 or more elements, of which // The content sequence has 5 or more elements, of which
// the `[4]` element contains the annotation data, so this is // the `[4]` element contains the annotation data, so this is
// checking that there is some annotation data present. // checking that there is some annotation data present.
if (!ContentSequence?.[4].ContentSequence?.length) { if (!ContentSequence?.[4]?.ContentSequence?.length) {
console.log('naturalizedReport missing imaging content', naturalizedReport); console.log('naturalizedReport missing imaging content', naturalizedReport);
throw new Error('Invalid report, no content'); throw new Error('Invalid report, no content');
} }
@ -128,22 +116,12 @@ const commandsModule = (props: withAppTypes) => {
} }
const onBeforeDicomStore = customizationService.getCustomization('onBeforeDicomStore'); const onBeforeDicomStore = customizationService.getCustomization('onBeforeDicomStore');
let dicomDict; let dicomDict;
if (typeof onBeforeDicomStore === 'function') { if (typeof onBeforeDicomStore === 'function') {
dicomDict = onBeforeDicomStore({ dicomDict, measurementData, naturalizedReport }); dicomDict = onBeforeDicomStore({ dicomDict, measurementData, naturalizedReport });
} }
await dataSource.store.dicom(naturalizedReport, null, dicomDict); await storeFn(naturalizedReport, { measurementData, dicomDict });
if (StudyInstanceUID) {
dataSource.deleteStudyMetadataPromise(StudyInstanceUID);
}
// The "Mode" route listens for DicomMetadataStore changes
// When a new instance is added, it listens and
// automatically calls makeDisplaySets
DicomMetadataStore.addInstances([naturalizedReport], true);
return naturalizedReport; return naturalizedReport;
} catch (error) { } catch (error) {
@ -166,7 +144,6 @@ const commandsModule = (props: withAppTypes) => {
}; };
const definitions = { const definitions = {
downloadReport: actions.downloadReport,
storeMeasurements: actions.storeMeasurements, storeMeasurements: actions.storeMeasurements,
hydrateStructuredReport: actions.hydrateStructuredReport, hydrateStructuredReport: actions.hydrateStructuredReport,
}; };

View File

@ -200,8 +200,13 @@ async function _load(
srDisplaySet.isRehydratable = isRehydratable(srDisplaySet, mappings); srDisplaySet.isRehydratable = isRehydratable(srDisplaySet, mappings);
srDisplaySet.isLoaded = true; srDisplaySet.isLoaded = true;
/** Check currently added displaySets and add measurements if the sources exist */ /** Check currently added displaySets and add measurements if the sources exist.
displaySetService.activeDisplaySets.forEach(activeDisplaySet => { * Walk the SR's study first in default series order (not load order) so SCOORD3D
* FrameOfReference matching picks a stable series when several share FOR. */
const displaySetsForSRPass = utils.sortDisplaySetsCopy(displaySetService.activeDisplaySets, {
studyInstanceUIDFirst: srDisplaySet.StudyInstanceUID,
});
displaySetsForSRPass.forEach(activeDisplaySet => {
_checkIfCanAddMeasurementsToDisplaySet( _checkIfCanAddMeasurementsToDisplaySet(
srDisplaySet, srDisplaySet,
activeDisplaySet, activeDisplaySet,
@ -637,16 +642,35 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
NUMContentItems.forEach(item => { NUMContentItems.forEach(item => {
const { ConceptNameCodeSequence, ContentSequence, MeasuredValueSequence } = item; const { ConceptNameCodeSequence, ContentSequence, MeasuredValueSequence } = item;
// Handle spatial reference ONLY if ContentSequence exists // Handle spatial reference ONLY if ContentSequence exists.
// ContentSequence may be a scalar SCOORD or an array when additional named
// SCOORDs (e.g. control points) are nested alongside the primary geometry.
// Pick the primary geometry entry: prefer the SCOORD without a
// ConceptNameCodeSequence (plain polyline), falling back to the first SCOORD.
if (ContentSequence) { if (ContentSequence) {
const { ValueType } = ContentSequence; const scoordItem = Array.isArray(ContentSequence)
? (ContentSequence.find(
cs =>
(cs.ValueType === 'SCOORD' || cs.ValueType === 'SCOORD3D') &&
!cs.ConceptNameCodeSequence
) ?? ContentSequence.find(cs => cs.ValueType === 'SCOORD' || cs.ValueType === 'SCOORD3D'))
: ContentSequence;
if (!scoordItem) {
console.warn(
'ContentSequence array contains no SCOORD or SCOORD3D entry, skipping annotation.'
);
return;
}
const { ValueType } = scoordItem;
if (ValueType !== 'SCOORD' && ValueType !== 'SCOORD3D') { if (ValueType !== 'SCOORD' && ValueType !== 'SCOORD3D') {
console.warn(`Graphic ${ValueType} not currently supported, skipping annotation.`); console.warn(`Graphic ${ValueType} not currently supported, skipping annotation.`);
return; return;
} }
const coords = _getCoordsFromSCOORDOrSCOORD3D(ContentSequence); const coords = _getCoordsFromSCOORDOrSCOORD3D(scoordItem);
if (coords) { if (coords) {
measurement.coords.push(coords); measurement.coords.push(coords);

View File

@ -69,7 +69,12 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
trackingUniqueIdentifiers.includes(annotation.data?.TrackingUniqueIdentifier) trackingUniqueIdentifiers.includes(annotation.data?.TrackingUniqueIdentifier)
); );
if (!viewport._actors?.size) { const hasActors =
typeof viewport.getActors === 'function'
? viewport.getActors().length > 0
: Boolean(viewport._actors?.size);
if (!hasActors) {
return; return;
} }

View File

@ -80,7 +80,7 @@ export default function hydrateStructuredReport(
const { ReferencedSOPInstanceUID, imageId, frameNumber = 1 } = measurement; const { ReferencedSOPInstanceUID, imageId, frameNumber = 1 } = measurement;
const key = `${ReferencedSOPInstanceUID}:${frameNumber}`; const key = `${ReferencedSOPInstanceUID}:${frameNumber}`;
if (!sopInstanceUIDToImageId[key]) { if (imageId && !sopInstanceUIDToImageId[key]) {
sopInstanceUIDToImageId[key] = imageId; sopInstanceUIDToImageId[key] = imageId;
} }
}); });
@ -118,41 +118,17 @@ export default function hydrateStructuredReport(
} }
}); });
// Set the series touched as tracked.
const imageIds = [];
// TODO: notification if no hydratable?
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType];
toolDataForAnnotationType.forEach(toolData => {
// Add the measurement to toolState
// dcmjs and Cornerstone3D has structural defect in supporting multi-frame
// files, and looking up the imageId from sopInstanceUIDToImageId results
// in the wrong value.
const frameNumber = toolData.annotation.data?.frameNumber || 1;
const imageId = sopInstanceUIDToImageId[`${toolData.sopInstanceUid}:${frameNumber}`];
if (!imageIds.includes(imageId)) {
imageIds.push(imageId);
}
});
});
let targetStudyInstanceUID; let targetStudyInstanceUID;
const SeriesInstanceUIDs = []; const SeriesInstanceUIDs = [];
for (let i = 0; i < imageIds.length; i++) { // Set the series touched as tracked.
const imageId = imageIds[i]; const imageIds = getImageIds(hydratableMeasurementsInSR, sopInstanceUIDToImageId);
if (!imageId) {
continue;
}
const { SeriesInstanceUID, StudyInstanceUID } = metaData.get('instance', imageId);
for (const imageId of imageIds) {
const { SeriesInstanceUID, StudyInstanceUID } = metaData.get('instance', imageId);
if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) { if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) {
SeriesInstanceUIDs.push(SeriesInstanceUID); SeriesInstanceUIDs.push(SeriesInstanceUID);
} }
if (!targetStudyInstanceUID) { if (!targetStudyInstanceUID) {
targetStudyInstanceUID = StudyInstanceUID; targetStudyInstanceUID = StudyInstanceUID;
} else if (targetStudyInstanceUID !== StudyInstanceUID) { } else if (targetStudyInstanceUID !== StudyInstanceUID) {
@ -160,6 +136,38 @@ export default function hydrateStructuredReport(
} }
} }
// For 3d annotations there are no image IDs,
// so we need to find the display sets by frame of reference to get the SeriesInstanceUIDs
const frameOfReferenceUIDs = getFrameOfReferenceUIDs(
hydratableMeasurementsInSR,
sopInstanceUIDToImageId
);
const displaySetsByFrameOfReferenceUID = new Map();
for (const FrameOfReferenceUID of frameOfReferenceUIDs) {
const displaySetsFOR = displaySetService.getDisplaySetsBy(
ds => ds.FrameOfReferenceUID === FrameOfReferenceUID && !ds.isDerivedDisplaySet
);
const ds = getReferencedDisplaySet(
displaySet,
displaySetsFOR,
FrameOfReferenceUID,
displaySetService
);
if (!ds) {
continue;
}
displaySetsByFrameOfReferenceUID.set(FrameOfReferenceUID, ds);
if (!SeriesInstanceUIDs.includes(ds.SeriesInstanceUID)) {
SeriesInstanceUIDs.push(ds.SeriesInstanceUID);
}
if (!targetStudyInstanceUID) {
targetStudyInstanceUID = ds.StudyInstanceUID;
} else if (targetStudyInstanceUID !== ds.StudyInstanceUID) {
console.warn('NO SUPPORT FOR SRs THAT HAVE MEASUREMENTS FROM MULTIPLE STUDIES.');
}
}
/** /**
* Gets reference data for what frame of reference and the referenced * Gets reference data for what frame of reference and the referenced
* image id, or for 3d measurements, the volumeId to apply this annotation to. * image id, or for 3d measurements, the volumeId to apply this annotation to.
@ -173,7 +181,7 @@ export default function hydrateStructuredReport(
const imageId = sopInstanceUIDToImageId[`${toolData.sopInstanceUid}:${frameNumber}`]; const imageId = sopInstanceUIDToImageId[`${toolData.sopInstanceUid}:${frameNumber}`];
if (!imageId) { if (!imageId) {
return getReferenceData3D(toolData, servicesManager); return getReferenceData3D(toolData, servicesManager, displaySetsByFrameOfReferenceUID);
} }
const instance = metaData.get('instance', imageId); const instance = metaData.get('instance', imageId);
@ -196,7 +204,7 @@ export default function hydrateStructuredReport(
toolDataForAnnotationType.forEach(toolData => { toolDataForAnnotationType.forEach(toolData => {
toolData.uid = guid(); toolData.uid = guid();
const referenceData = getReferenceData(toolData); const referenceData = getReferenceData(toolData);
const { imageId } = referenceData; const { referencedImageId } = referenceData;
const annotation = { const annotation = {
annotationUID: toolData.annotation.annotationUID, annotationUID: toolData.annotation.annotationUID,
@ -241,8 +249,8 @@ export default function hydrateStructuredReport(
locking.setAnnotationLocked(newAnnotationUID, true); locking.setAnnotationLocked(newAnnotationUID, true);
} }
if (imageId && !imageIds.includes(imageId)) { if (referencedImageId && !imageIds.includes(referencedImageId)) {
imageIds.push(imageId); imageIds.push(referencedImageId);
} }
}); });
}); });
@ -255,32 +263,120 @@ export default function hydrateStructuredReport(
}; };
} }
/**
* Gets the unique imageIds from hydratable measurements that have an imageId reference
* (i.e., 2D/SCOORD annotations).
*/
function getImageIds(hydratableMeasurementsInSR, sopInstanceUIDToImageId): string[] {
const imageIds: string[] = [];
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType];
toolDataForAnnotationType.forEach(toolData => {
// Add the measurement to toolState
// dcmjs and Cornerstone3D has structural defect in supporting multi-frame
// files, and looking up the imageId from sopInstanceUIDToImageId results
// in the wrong value.
const frameNumber = toolData.annotation.data?.frameNumber || 1;
const imageId = sopInstanceUIDToImageId[`${toolData.sopInstanceUid}:${frameNumber}`];
if (imageId && !imageIds.includes(imageId)) {
imageIds.push(imageId);
}
});
});
return imageIds;
}
/**
* Gets the unique FrameOfReferenceUIDs from hydratable measurements that have no imageId reference
* (i.e., 3D/SCOORD3D annotations). This excludes annotations handled by the getImageIds function.
*/
function getFrameOfReferenceUIDs(hydratableMeasurementsInSR, sopInstanceUIDToImageId): string[] {
const frameOfReferenceUIDs: string[] = [];
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType];
toolDataForAnnotationType.forEach(toolData => {
const frameNumber = toolData.annotation.data?.frameNumber || 1;
const imageId = sopInstanceUIDToImageId[`${toolData.sopInstanceUid}:${frameNumber}`];
if (!imageId) {
const { FrameOfReferenceUID } = toolData.annotation.metadata;
if (FrameOfReferenceUID && !frameOfReferenceUIDs.includes(FrameOfReferenceUID)) {
frameOfReferenceUIDs.push(FrameOfReferenceUID);
}
}
});
});
return frameOfReferenceUIDs;
}
/** /**
* For 3d annotations, there are often several display sets which could * For 3d annotations, there are often several display sets which could
* be used to display the annotation. Choose the first annotation with the * be used to display the annotation. Choose the first annotation with the
* same frame of reference that is reconstructable, or the first display set * same frame of reference that is reconstructable, or the first display set
* otherwise. * otherwise.
*/ */
function chooseDisplaySet(displaySets, annotation) { function chooseDisplaySet(displaySets, reference) {
if (!displaySets?.length) { if (!displaySets?.length) {
console.warn('No display set found for', annotation); console.warn('No display set found for', reference);
return; return;
} }
if (displaySets.length === 1) { const sortedDisplaySets = OHIF.utils.sortDisplaySetsCopy(displaySets);
return displaySets[0]; if (sortedDisplaySets.length === 1) {
return sortedDisplaySets[0];
} }
const volumeDs = displaySets.find(ds => ds.isReconstructable); const volumeDs = sortedDisplaySets.find(ds => ds.isReconstructable);
if (volumeDs) { if (volumeDs) {
return volumeDs; return volumeDs;
} }
return displaySets[0]; return sortedDisplaySets[0];
}
/**
* SCOORD3D only identifies a frame of reference, so many series can be valid
* candidates. The SR loader has already selected and recorded a stable display
* set for each measurement. Reuse that selection during hydration so the
* viewport series and annotation volume cannot depend on display-set load order.
*/
function getReferencedDisplaySet(
srDisplaySet,
displaySets,
FrameOfReferenceUID,
displaySetService
) {
const referencedDisplaySetInstanceUID = srDisplaySet.measurements?.find(measurement =>
measurement.coords?.some(
coord =>
coord.ValueType === 'SCOORD3D' &&
coord.ReferencedFrameOfReferenceSequence === FrameOfReferenceUID
)
)?.displaySetInstanceUID;
const referencedDisplaySet = referencedDisplaySetInstanceUID
? displaySetService.getDisplaySetByUID(referencedDisplaySetInstanceUID)
: undefined;
if (
referencedDisplaySet?.FrameOfReferenceUID === FrameOfReferenceUID &&
!referencedDisplaySet.isDerivedDisplaySet
) {
return referencedDisplaySet;
}
return chooseDisplaySet(displaySets, FrameOfReferenceUID);
} }
/** /**
* Gets the additional reference data appropriate for a 3d reference. * Gets the additional reference data appropriate for a 3d reference.
* This will choose a volume id, frame of reference and a plane restriction. * This will choose a volume id, frame of reference and a plane restriction.
*/ */
function getReferenceData3D(toolData, servicesManager: Types.ServicesManager) { function getReferenceData3D(
toolData,
servicesManager: Types.ServicesManager,
displaySetsByFrameOfReferenceUID = new Map()
) {
const { FrameOfReferenceUID } = toolData.annotation.metadata; const { FrameOfReferenceUID } = toolData.annotation.metadata;
const { points } = toolData.annotation.data.handles; const { points } = toolData.annotation.data.handles;
const { displaySetService } = servicesManager.services; const { displaySetService } = servicesManager.services;
@ -292,7 +388,9 @@ function getReferenceData3D(toolData, servicesManager: Types.ServicesManager) {
FrameOfReferenceUID, FrameOfReferenceUID,
}; };
} }
const ds = chooseDisplaySet(displaySetsFOR, toolData.annotation); const ds =
displaySetsByFrameOfReferenceUID.get(FrameOfReferenceUID) ||
chooseDisplaySet(displaySetsFOR, toolData.annotation);
const cameraView = chooseCameraView(ds, points); const cameraView = chooseCameraView(ds, points);
const viewReference = { const viewReference = {

View File

@ -1,8 +1,8 @@
const webpack = require('webpack'); const webpack = require('@rspack/core');
const { merge } = require('webpack-merge'); const { merge } = require('webpack-merge');
const path = require('path'); const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js'); const webpackCommon = require('./../../../.webpack/webpack.base.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = webpack.CssExtractRspackPlugin;
const pkg = require('./../package.json'); const pkg = require('./../package.json');
@ -35,9 +35,11 @@ module.exports = (env, argv) => {
sideEffects: true, sideEffects: true,
}, },
output: { output: {
library: {
name: 'ohif-extension-cornerstone',
type: 'umd',
},
path: ROOT_DIR, path: ROOT_DIR,
library: 'ohif-extension-cornerstone',
libraryTarget: 'umd',
filename: pkg.main, filename: pkg.main,
}, },
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],

View File

@ -3,6 +3,619 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.13.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.88...v3.13.0-beta.89) (2026-06-10)
### Bug Fixes
* ohif tests to run with cornerstone 3d 5.0 ([#6043](https://github.com/OHIF/Viewers/issues/6043)) ([6dd150d](https://github.com/OHIF/Viewers/commit/6dd150d401ad73d60632a23378b7dfd4b5142690))
# [3.13.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.87...v3.13.0-beta.88) (2026-06-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.86...v3.13.0-beta.87) (2026-05-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.85...v3.13.0-beta.86) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.84...v3.13.0-beta.85) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.83...v3.13.0-beta.84) (2026-05-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.82...v3.13.0-beta.83) (2026-05-26)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.81...v3.13.0-beta.82) (2026-05-22)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.80...v3.13.0-beta.81) (2026-05-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.79...v3.13.0-beta.80) (2026-05-20)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.78...v3.13.0-beta.79) (2026-05-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.77...v3.13.0-beta.78) (2026-05-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.76...v3.13.0-beta.77) (2026-05-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.75...v3.13.0-beta.76) (2026-05-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.74...v3.13.0-beta.75) (2026-05-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.73...v3.13.0-beta.74) (2026-05-15)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.72...v3.13.0-beta.73) (2026-05-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.71...v3.13.0-beta.72) (2026-05-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.70...v3.13.0-beta.71) (2026-05-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.69...v3.13.0-beta.70) (2026-05-07)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.68...v3.13.0-beta.69) (2026-05-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.67...v3.13.0-beta.68) (2026-05-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.66...v3.13.0-beta.67) (2026-04-30)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.65...v3.13.0-beta.66) (2026-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.64...v3.13.0-beta.65) (2026-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.63...v3.13.0-beta.64) (2026-04-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.62...v3.13.0-beta.63) (2026-04-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.61...v3.13.0-beta.62) (2026-04-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.60...v3.13.0-beta.61) (2026-04-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.59...v3.13.0-beta.60) (2026-04-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.58...v3.13.0-beta.59) (2026-04-16)
### Bug Fixes
* Use newer ONNX version and load without errors ([#5854](https://github.com/OHIF/Viewers/issues/5854)) ([cc5dc20](https://github.com/OHIF/Viewers/commit/cc5dc201649bcbfb4cfad21141dbd56000d30f53)), closes [#5875](https://github.com/OHIF/Viewers/issues/5875) [#5884](https://github.com/OHIF/Viewers/issues/5884) [#5886](https://github.com/OHIF/Viewers/issues/5886) [#5893](https://github.com/OHIF/Viewers/issues/5893) [#5874](https://github.com/OHIF/Viewers/issues/5874) [#5865](https://github.com/OHIF/Viewers/issues/5865)
# [3.13.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.57...v3.13.0-beta.58) (2026-04-14)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.56...v3.13.0-beta.57) (2026-04-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.55...v3.13.0-beta.56) (2026-04-10)
### Bug Fixes
* A couple of changes to enable cs3d integration build ([#5944](https://github.com/OHIF/Viewers/issues/5944)) ([f6bbd5c](https://github.com/OHIF/Viewers/commit/f6bbd5c779e4692dc47c93327a890661b8dcc174))
# [3.13.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.54...v3.13.0-beta.55) (2026-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.53...v3.13.0-beta.54) (2026-04-08)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.52...v3.13.0-beta.53) (2026-04-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.51...v3.13.0-beta.52) (2026-04-06)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.50...v3.13.0-beta.51) (2026-04-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.49...v3.13.0-beta.50) (2026-04-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.48...v3.13.0-beta.49) (2026-04-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.47...v3.13.0-beta.48) (2026-04-01)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.46...v3.13.0-beta.47) (2026-03-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.45...v3.13.0-beta.46) (2026-03-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.44...v3.13.0-beta.45) (2026-03-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.43...v3.13.0-beta.44) (2026-03-27)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.42...v3.13.0-beta.43) (2026-03-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.41...v3.13.0-beta.42) (2026-03-17)
### Features
* Add combined build ([#5895](https://github.com/OHIF/Viewers/issues/5895)) ([1df671e](https://github.com/OHIF/Viewers/commit/1df671e9ab67d43f98bf09f6838ddc0a5f220277))
# [3.13.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.40...v3.13.0-beta.41) (2026-03-16)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.39...v3.13.0-beta.40) (2026-03-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.38...v3.13.0-beta.39) (2026-03-13)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.37...v3.13.0-beta.38) (2026-03-12)
### Bug Fixes
* **segmentation:** Display "No description S:{series number} {modality}" for segmentations with no label. ([#5874](https://github.com/OHIF/Viewers/issues/5874)) ([7b5d0ce](https://github.com/OHIF/Viewers/commit/7b5d0ce40e5422ccfce6091ac3d7559586cbe57e))
# [3.13.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.36...v3.13.0-beta.37) (2026-03-12)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.35...v3.13.0-beta.36) (2026-03-11)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.34...v3.13.0-beta.35) (2026-03-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.33...v3.13.0-beta.34) (2026-03-09)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.32...v3.13.0-beta.33) (2026-03-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.31...v3.13.0-beta.32) (2026-03-05)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.30...v3.13.0-beta.31) (2026-03-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.29...v3.13.0-beta.30) (2026-03-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.28...v3.13.0-beta.29) (2026-03-04)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.27...v3.13.0-beta.28) (2026-03-03)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.26...v3.13.0-beta.27) (2026-03-02)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.25...v3.13.0-beta.26) (2026-02-28)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.24...v3.13.0-beta.25) (2026-02-26)
### Bug Fixes
* Update CS3D 4.18.2 ([#5837](https://github.com/OHIF/Viewers/issues/5837)) ([5370b93](https://github.com/OHIF/Viewers/commit/5370b9382e7bfddeadd69bab9e653f5f997a6e19))
# [3.13.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.23...v3.13.0-beta.24) (2026-02-24)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.22...v3.13.0-beta.23) (2026-02-23)
### Bug Fixes
* palette color 8 encoded 16 ([#5823](https://github.com/OHIF/Viewers/issues/5823)) ([eeca1f7](https://github.com/OHIF/Viewers/commit/eeca1f7a7b529d1700f5d281a6fd98074547ffe9)), closes [#5813](https://github.com/OHIF/Viewers/issues/5813)
# [3.13.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.21...v3.13.0-beta.22) (2026-02-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.20...v3.13.0-beta.21) (2026-02-23)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.19...v3.13.0-beta.20) (2026-02-21)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.18...v3.13.0-beta.19) (2026-02-20)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.17...v3.13.0-beta.18) (2026-02-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.16...v3.13.0-beta.17) (2026-02-19)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.15...v3.13.0-beta.16) (2026-02-18)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
# [3.13.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.14...v3.13.0-beta.15) (2026-02-17) # [3.13.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.14...v3.13.0-beta.15) (2026-02-17)
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume **Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume

View File

@ -1,12 +1,15 @@
{ {
"name": "@ohif/extension-cornerstone-dynamic-volume", "name": "@ohif/extension-cornerstone-dynamic-volume",
"version": "3.13.0-beta.15", "version": "3.13.0-beta.125",
"description": "OHIF extension for 4D volumes data", "description": "OHIF extension for 4D volumes data",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
"repository": "OHIF/Viewers", "repository": "OHIF/Viewers",
"main": "dist/ohif-extension-cornerstone-dynamic-volume.umd.js", "main": "dist/ohif-extension-cornerstone-dynamic-volume.umd.js",
"module": "src/index.ts", "module": "src/index.ts",
"engines": {
"node": ">=24"
},
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./types": "./src/types/index.ts" "./types": "./src/types/index.ts"
@ -19,31 +22,34 @@
"access": "public" "access": "public"
}, },
"scripts": { "scripts": {
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", "dev": "cross-env NODE_ENV=development rspack build --config .webpack/webpack.dev.js --watch",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js", "build": "cross-env NODE_ENV=production rspack build --config .webpack/webpack.prod.js",
"build:package": "yarn run build", "build:package": "pnpm run build",
"clean": "shx rm -rf dist", "clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules", "clean:deep": "pnpm run clean && shx rm -rf node_modules",
"start": "yarn run dev", "start": "pnpm run dev",
"test:unit": "jest --watchAll", "test:unit": "jest --watchAll",
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
}, },
"peerDependencies": { "peerDependencies": {
"@ohif/core": "3.13.0-beta.15", "@ohif/core": "workspace:*",
"@ohif/extension-cornerstone": "3.13.0-beta.15", "@ohif/extension-cornerstone": "workspace:*",
"@ohif/extension-default": "3.13.0-beta.15", "@ohif/extension-default": "workspace:*",
"@ohif/i18n": "3.13.0-beta.15", "@ohif/i18n": "workspace:*",
"@ohif/ui": "3.13.0-beta.15", "@ohif/ui": "workspace:*",
"dcmjs": "0.49.4", "dcmjs": "0.52.0",
"dicom-parser": "1.8.21", "dicom-parser": "1.8.21",
"hammerjs": "2.0.8", "hammerjs": "2.0.8",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"react": "18.3.1" "react": "18.3.1"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "7.28.2", "@babel/runtime": "7.29.7",
"@cornerstonejs/core": "4.15.29", "@cornerstonejs/core": "5.4.17",
"@cornerstonejs/tools": "4.15.29", "@cornerstonejs/tools": "5.4.17",
"classnames": "2.5.1" "classnames": "2.5.1"
},
"devDependencies": {
"cross-env": "7.0.3"
} }
} }

View File

@ -1,8 +1,8 @@
const webpack = require('webpack'); const webpack = require('@rspack/core');
const { merge } = require('webpack-merge'); const { merge } = require('webpack-merge');
const path = require('path'); const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js'); const webpackCommon = require('./../../../.webpack/webpack.base.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = webpack.CssExtractRspackPlugin;
const pkg = require('./../package.json'); const pkg = require('./../package.json');
@ -35,9 +35,11 @@ module.exports = (env, argv) => {
sideEffects: true, sideEffects: true,
}, },
output: { output: {
library: {
name: 'ohif-extension-cornerstone',
type: 'umd',
},
path: ROOT_DIR, path: ROOT_DIR,
library: 'ohif-extension-cornerstone',
libraryTarget: 'umd',
filename: pkg.main, filename: pkg.main,
}, },
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/], externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],

View File

@ -3,6 +3,676 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.13.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.88...v3.13.0-beta.89) (2026-06-10)
### Bug Fixes
* ohif tests to run with cornerstone 3d 5.0 ([#6043](https://github.com/OHIF/Viewers/issues/6043)) ([6dd150d](https://github.com/OHIF/Viewers/commit/6dd150d401ad73d60632a23378b7dfd4b5142690))
# [3.13.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.87...v3.13.0-beta.88) (2026-06-05)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.86...v3.13.0-beta.87) (2026-05-29)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.85...v3.13.0-beta.86) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.84...v3.13.0-beta.85) (2026-05-28)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.83...v3.13.0-beta.84) (2026-05-27)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.82...v3.13.0-beta.83) (2026-05-26)
### Bug Fixes
* **segmentation:** restore navigation for duplicated contour segments ([#6038](https://github.com/OHIF/Viewers/issues/6038)) ([0c1ca6b](https://github.com/OHIF/Viewers/commit/0c1ca6b04a6c1aef478c7a7d9f360f9b9c50f188))
# [3.13.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.81...v3.13.0-beta.82) (2026-05-22)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.80...v3.13.0-beta.81) (2026-05-21)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.79...v3.13.0-beta.80) (2026-05-20)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.78...v3.13.0-beta.79) (2026-05-19)
### Bug Fixes
* **seg:** prevent viewport orientation change when loading SEG in manual grid layout ([#6021](https://github.com/OHIF/Viewers/issues/6021)) ([95251ab](https://github.com/OHIF/Viewers/commit/95251ab6709934e4ef59931379f8d9d7629f87b2))
# [3.13.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.77...v3.13.0-beta.78) (2026-05-19)
### Bug Fixes
* **measurements:** read displayName in SplineROI and PlanarFreehandROI reports ([#5908](https://github.com/OHIF/Viewers/issues/5908)) ([27af682](https://github.com/OHIF/Viewers/commit/27af6821b56f2c5be3a5d4ce38d05be6fbce68f4))
# [3.13.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.76...v3.13.0-beta.77) (2026-05-19)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.75...v3.13.0-beta.76) (2026-05-19)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.74...v3.13.0-beta.75) (2026-05-18)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.73...v3.13.0-beta.74) (2026-05-15)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.72...v3.13.0-beta.73) (2026-05-13)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.71...v3.13.0-beta.72) (2026-05-12)
### Bug Fixes
* **measurement-tracking:** restore tracked state on undo after Delete all ([#5994](https://github.com/OHIF/Viewers/issues/5994)) ([397aa4d](https://github.com/OHIF/Viewers/commit/397aa4d0e361e95dcbd83e0557a9cd84c0b8440a))
# [3.13.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.70...v3.13.0-beta.71) (2026-05-07)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.69...v3.13.0-beta.70) (2026-05-07)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.68...v3.13.0-beta.69) (2026-05-06)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.67...v3.13.0-beta.68) (2026-05-05)
### Bug Fixes
* **ToolGroupService:** Centralize tool binding persistence and provide API to add and remove persisted bindings. ([#5989](https://github.com/OHIF/Viewers/issues/5989)) ([979d619](https://github.com/OHIF/Viewers/commit/979d619f7ccb426beb75d59721b01c98175d5241))
# [3.13.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.66...v3.13.0-beta.67) (2026-04-30)
### Features
* **ui-next:** Adds toggle state for ToolButton and Crosshair example ([#5914](https://github.com/OHIF/Viewers/issues/5914)) ([691e267](https://github.com/OHIF/Viewers/commit/691e26731f4f3b3957fe81fc051b09c972696cf6))
# [3.13.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.65...v3.13.0-beta.66) (2026-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.64...v3.13.0-beta.65) (2026-04-29)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.63...v3.13.0-beta.64) (2026-04-27)
### Bug Fixes
* **seg:** prevent segmentations from spreading to all viewports before hydration confirmation in 3D four-up ([#5967](https://github.com/OHIF/Viewers/issues/5967)) ([f8ccf9f](https://github.com/OHIF/Viewers/commit/f8ccf9ff2ea9ab7c38bd427514a8ae87902822a3))
# [3.13.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.62...v3.13.0-beta.63) (2026-04-27)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.61...v3.13.0-beta.62) (2026-04-23)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.60...v3.13.0-beta.61) (2026-04-23)
### Features
* **slice scrollbar:** Integrate ViewportSliceProgressScrollbar with customizations and docs ([#5960](https://github.com/OHIF/Viewers/issues/5960)) ([8fc0dc1](https://github.com/OHIF/Viewers/commit/8fc0dc16e97f3262fdac902ecf2d7c20cb8f78cc))
# [3.13.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.59...v3.13.0-beta.60) (2026-04-21)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.58...v3.13.0-beta.59) (2026-04-16)
### Bug Fixes
* Use newer ONNX version and load without errors ([#5854](https://github.com/OHIF/Viewers/issues/5854)) ([cc5dc20](https://github.com/OHIF/Viewers/commit/cc5dc201649bcbfb4cfad21141dbd56000d30f53)), closes [#5875](https://github.com/OHIF/Viewers/issues/5875) [#5884](https://github.com/OHIF/Viewers/issues/5884) [#5886](https://github.com/OHIF/Viewers/issues/5886) [#5893](https://github.com/OHIF/Viewers/issues/5893) [#5874](https://github.com/OHIF/Viewers/issues/5874) [#5865](https://github.com/OHIF/Viewers/issues/5865)
# [3.13.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.57...v3.13.0-beta.58) (2026-04-14)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.56...v3.13.0-beta.57) (2026-04-13)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.56](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.55...v3.13.0-beta.56) (2026-04-10)
### Bug Fixes
* A couple of changes to enable cs3d integration build ([#5944](https://github.com/OHIF/Viewers/issues/5944)) ([f6bbd5c](https://github.com/OHIF/Viewers/commit/f6bbd5c779e4692dc47c93327a890661b8dcc174))
# [3.13.0-beta.55](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.54...v3.13.0-beta.55) (2026-04-10)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.54](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.53...v3.13.0-beta.54) (2026-04-08)
### Bug Fixes
* **cornerstone:** read FrameOfReferenceUID from display set in viewport service ([#5950](https://github.com/OHIF/Viewers/issues/5950)) ([d219171](https://github.com/OHIF/Viewers/commit/d219171e25caf5a7c575080f63819d6f4fe45016))
# [3.13.0-beta.53](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.52...v3.13.0-beta.53) (2026-04-06)
### Reverts
* rename DisplaySet.frameOfReferenceUID back to FrameOfReferenceUID ([#5943](https://github.com/OHIF/Viewers/issues/5943)) ([0e933c2](https://github.com/OHIF/Viewers/commit/0e933c256e07b7cda35ed2ba3cfb1ab35d895d57))
# [3.13.0-beta.52](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.51...v3.13.0-beta.52) (2026-04-06)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.51](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.50...v3.13.0-beta.51) (2026-04-03)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.50](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.49...v3.13.0-beta.50) (2026-04-02)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.49](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.48...v3.13.0-beta.49) (2026-04-02)
### Bug Fixes
* **segmentation:** restrict overlay segmentation menu to same frame of reference as viewport background display set ([#5900](https://github.com/OHIF/Viewers/issues/5900)) ([b9029ef](https://github.com/OHIF/Viewers/commit/b9029ef6f8d63a0e36ec929310c1c5ad3f563ef8))
# [3.13.0-beta.48](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.47...v3.13.0-beta.48) (2026-04-01)
### Bug Fixes
* **measurement:** Restore viewport interactivity when deleting in-progress Spline or Livewire measurement ([#5905](https://github.com/OHIF/Viewers/issues/5905)) ([7929f08](https://github.com/OHIF/Viewers/commit/7929f0898f1bf882682a912b1c32d93d0944726f))
# [3.13.0-beta.47](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.46...v3.13.0-beta.47) (2026-03-28)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.46](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.45...v3.13.0-beta.46) (2026-03-27)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.45](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.44...v3.13.0-beta.45) (2026-03-27)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.44](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.43...v3.13.0-beta.44) (2026-03-27)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.43](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.42...v3.13.0-beta.43) (2026-03-17)
### Bug Fixes
* **SR:** Added support for spline and live wire SR items. ([#5870](https://github.com/OHIF/Viewers/issues/5870)) ([1d4802c](https://github.com/OHIF/Viewers/commit/1d4802c2a38dd75dca7afca87913e100453c9135))
# [3.13.0-beta.42](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.41...v3.13.0-beta.42) (2026-03-17)
### Features
* Add combined build ([#5895](https://github.com/OHIF/Viewers/issues/5895)) ([1df671e](https://github.com/OHIF/Viewers/commit/1df671e9ab67d43f98bf09f6838ddc0a5f220277))
# [3.13.0-beta.41](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.40...v3.13.0-beta.41) (2026-03-16)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.40](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.39...v3.13.0-beta.40) (2026-03-13)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.39](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.38...v3.13.0-beta.39) (2026-03-13)
### Bug Fixes
* **window level:** The window level value is not displayed by default on all the viewports when selecting common/custom layout and TMTV. ([#5865](https://github.com/OHIF/Viewers/issues/5865)) ([fe1ecfe](https://github.com/OHIF/Viewers/commit/fe1ecfe0cc5d1bcf82686d821f356da8936a8c4b))
# [3.13.0-beta.38](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.37...v3.13.0-beta.38) (2026-03-12)
### Bug Fixes
* **segmentation:** Display "No description S:{series number} {modality}" for segmentations with no label. ([#5874](https://github.com/OHIF/Viewers/issues/5874)) ([7b5d0ce](https://github.com/OHIF/Viewers/commit/7b5d0ce40e5422ccfce6091ac3d7559586cbe57e))
# [3.13.0-beta.37](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.36...v3.13.0-beta.37) (2026-03-12)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.36](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.35...v3.13.0-beta.36) (2026-03-11)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.35](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.34...v3.13.0-beta.35) (2026-03-09)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.34](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.33...v3.13.0-beta.34) (2026-03-09)
### Bug Fixes
* **seg hydration:** auto-hydrate RT struct on second load with disableConfirmationPrompts ([#5875](https://github.com/OHIF/Viewers/issues/5875)) ([6f773f9](https://github.com/OHIF/Viewers/commit/6f773f997214c388409f3af3e9fc0e5b84debc10))
# [3.13.0-beta.33](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.32...v3.13.0-beta.33) (2026-03-05)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.32](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.31...v3.13.0-beta.32) (2026-03-05)
### Features
* **ecg:** add DICOM ECG waveform extension ([#5856](https://github.com/OHIF/Viewers/issues/5856)) ([70f76ae](https://github.com/OHIF/Viewers/commit/70f76aeba1b27f8e4de5ec73a7b831428e38e3d2))
# [3.13.0-beta.31](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.30...v3.13.0-beta.31) (2026-03-04)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.30](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.29...v3.13.0-beta.30) (2026-03-04)
### Bug Fixes
* **segmentation:** segment bidirectional tool error and show toast notification when no segment is drawn ([#5861](https://github.com/OHIF/Viewers/issues/5861)) ([7e10830](https://github.com/OHIF/Viewers/commit/7e10830d5802e22bda4df6a3f8008e9a414b4cd4))
# [3.13.0-beta.29](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.28...v3.13.0-beta.29) (2026-03-04)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.28](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.27...v3.13.0-beta.28) (2026-03-03)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.27](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.26...v3.13.0-beta.27) (2026-03-02)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.26](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.25...v3.13.0-beta.26) (2026-02-28)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.25](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.24...v3.13.0-beta.25) (2026-02-26)
### Bug Fixes
* Update CS3D 4.18.2 ([#5837](https://github.com/OHIF/Viewers/issues/5837)) ([5370b93](https://github.com/OHIF/Viewers/commit/5370b9382e7bfddeadd69bab9e653f5f997a6e19))
# [3.13.0-beta.24](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.23...v3.13.0-beta.24) (2026-02-24)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.23](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.22...v3.13.0-beta.23) (2026-02-23)
### Bug Fixes
* palette color 8 encoded 16 ([#5823](https://github.com/OHIF/Viewers/issues/5823)) ([eeca1f7](https://github.com/OHIF/Viewers/commit/eeca1f7a7b529d1700f5d281a6fd98074547ffe9)), closes [#5813](https://github.com/OHIF/Viewers/issues/5813)
# [3.13.0-beta.22](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.21...v3.13.0-beta.22) (2026-02-23)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.21](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.20...v3.13.0-beta.21) (2026-02-23)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.20](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.19...v3.13.0-beta.20) (2026-02-21)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.19](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.18...v3.13.0-beta.19) (2026-02-20)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.18](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.17...v3.13.0-beta.18) (2026-02-19)
### Bug Fixes
* **segmentation overlay:** update viewport ds list upon seg delete - OHIF-2425 ([#5729](https://github.com/OHIF/Viewers/issues/5729)) ([b353541](https://github.com/OHIF/Viewers/commit/b35354120a01d89b27844f967c32c052bba8b096))
# [3.13.0-beta.17](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.16...v3.13.0-beta.17) (2026-02-19)
### Bug Fixes
* Add copy to clipboard option for capture ([#5720](https://github.com/OHIF/Viewers/issues/5720)) ([53c67f3](https://github.com/OHIF/Viewers/commit/53c67f361234a0b4160c1b11fafcb143a0723336))
# [3.13.0-beta.16](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.15...v3.13.0-beta.16) (2026-02-18)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [3.13.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.14...v3.13.0-beta.15) (2026-02-17) # [3.13.0-beta.15](https://github.com/OHIF/Viewers/compare/v3.13.0-beta.14...v3.13.0-beta.15) (2026-02-17)
**Note:** Version bump only for package @ohif/extension-cornerstone **Note:** Version bump only for package @ohif/extension-cornerstone

View File

@ -5,8 +5,6 @@ module.exports = {
moduleNameMapper: { moduleNameMapper: {
...base.moduleNameMapper, ...base.moduleNameMapper,
'@ohif/(.*)': '<rootDir>/../../platform/$1/src', '@ohif/(.*)': '<rootDir>/../../platform/$1/src',
'^@cornerstonejs/([^/]+)/(.*)$': '<rootDir>/../../node_modules/@cornerstonejs/$1/dist/esm/$2',
'^@cornerstonejs/([^/]+)$': '<rootDir>/../../node_modules/@cornerstonejs/$1/dist/esm',
}, },
// rootDir: "../.." // rootDir: "../.."
// testMatch: [ // testMatch: [

View File

@ -1,6 +1,6 @@
{ {
"name": "@ohif/extension-cornerstone", "name": "@ohif/extension-cornerstone",
"version": "3.13.0-beta.15", "version": "3.13.0-beta.125",
"description": "OHIF extension for Cornerstone", "description": "OHIF extension for Cornerstone",
"author": "OHIF", "author": "OHIF",
"license": "MIT", "license": "MIT",
@ -13,9 +13,7 @@
"./types": "./src/types/index.ts" "./types": "./src/types/index.ts"
}, },
"engines": { "engines": {
"node": ">=10", "node": ">=24"
"npm": ">=6",
"yarn": ">=1.16.0"
}, },
"files": [ "files": [
"dist", "dist",
@ -26,12 +24,12 @@
}, },
"scripts": { "scripts": {
"clean": "shx rm -rf dist", "clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules", "clean:deep": "pnpm run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo", "dev": "cross-env NODE_ENV=development rspack build --config .webpack/webpack.dev.js --watch",
"dev:cornerstone": "yarn run dev", "dev:cornerstone": "pnpm run dev",
"build": "cross-env NODE_ENV=production webpack --progress --config .webpack/webpack.prod.js", "build": "cross-env NODE_ENV=production rspack build --config .webpack/webpack.prod.js",
"build:package-1": "yarn run build", "build:package-1": "pnpm run build",
"start": "yarn run dev", "start": "pnpm run dev",
"test:unit": "jest --watchAll", "test:unit": "jest --watchAll",
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests" "test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
}, },
@ -40,10 +38,11 @@
"@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2", "@cornerstonejs/codec-libjpeg-turbo-8bit": "1.2.2",
"@cornerstonejs/codec-openjpeg": "1.3.0", "@cornerstonejs/codec-openjpeg": "1.3.0",
"@cornerstonejs/codec-openjph": "2.4.7", "@cornerstonejs/codec-openjph": "2.4.7",
"@cornerstonejs/dicom-image-loader": "4.15.29", "@cornerstonejs/dicom-image-loader": "5.4.17",
"@ohif/core": "3.13.0-beta.15", "@ohif/core": "workspace:*",
"@ohif/ui": "3.13.0-beta.15", "@ohif/extension-default": "workspace:*",
"dcmjs": "0.49.4", "@ohif/ui": "workspace:*",
"dcmjs": "0.52.0",
"dicom-parser": "1.8.21", "dicom-parser": "1.8.21",
"hammerjs": "2.0.8", "hammerjs": "2.0.8",
"prop-types": "15.8.1", "prop-types": "15.8.1",
@ -52,16 +51,17 @@
"react-resize-detector": "10.0.1" "react-resize-detector": "10.0.1"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "7.28.2", "@babel/runtime": "7.29.7",
"@cornerstonejs/adapters": "4.15.29", "@cornerstonejs/adapters": "5.4.17",
"@cornerstonejs/ai": "4.15.29", "@cornerstonejs/ai": "5.4.17",
"@cornerstonejs/core": "4.15.29", "@cornerstonejs/core": "5.4.17",
"@cornerstonejs/labelmap-interpolation": "4.15.29", "@cornerstonejs/labelmap-interpolation": "5.4.17",
"@cornerstonejs/polymorphic-segmentation": "4.15.29", "@cornerstonejs/metadata": "5.4.17",
"@cornerstonejs/tools": "4.15.29", "@cornerstonejs/polymorphic-segmentation": "5.4.17",
"@cornerstonejs/tools": "5.4.17",
"@icr/polyseg-wasm": "0.4.0", "@icr/polyseg-wasm": "0.4.0",
"@itk-wasm/morphological-contour-interpolation": "1.1.0", "@itk-wasm/morphological-contour-interpolation": "1.1.0",
"@kitware/vtk.js": "34.15.1", "@kitware/vtk.js": "35.5.3",
"html2canvas": "1.4.1", "html2canvas": "1.4.1",
"immutability-helper": "3.1.1", "immutability-helper": "3.1.1",
"lodash.compact": "3.0.1", "lodash.compact": "3.0.1",
@ -70,5 +70,8 @@
"lodash.zip": "4.2.0", "lodash.zip": "4.2.0",
"shader-loader": "1.3.1", "shader-loader": "1.3.1",
"worker-loader": "3.0.8" "worker-loader": "3.0.8"
},
"devDependencies": {
"cross-env": "7.0.3"
} }
} }

View File

@ -1,13 +1,14 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import ViewportImageScrollbar from './ViewportImageScrollbar'; import ViewportImageScrollbar from './ViewportImageScrollbar';
import ViewportSliceProgressScrollbar from './ViewportSliceProgressScrollbar/ViewportSliceProgressScrollbar';
import CustomizableViewportOverlay from './CustomizableViewportOverlay'; import CustomizableViewportOverlay from './CustomizableViewportOverlay';
import ViewportOrientationMarkers from './ViewportOrientationMarkers'; import ViewportOrientationMarkers from './ViewportOrientationMarkers';
import ViewportImageSliceLoadingIndicator from './ViewportImageSliceLoadingIndicator'; import ViewportImageSliceLoadingIndicator from './ViewportImageSliceLoadingIndicator';
function CornerstoneOverlays(props: withAppTypes) { function CornerstoneOverlays(props: withAppTypes) {
const { viewportId, element, scrollbarHeight, servicesManager } = props; const { viewportId, element, scrollbarHeight, servicesManager } = props;
const { cornerstoneViewportService } = servicesManager.services; const { cornerstoneViewportService, customizationService } = servicesManager.services;
const [imageSliceData, setImageSliceData] = useState({ const [imageSliceData, setImageSliceData] = useState({
imageIndex: 0, imageIndex: 0,
numberOfSlices: 0, numberOfSlices: 0,
@ -43,17 +44,31 @@ function CornerstoneOverlays(props: withAppTypes) {
} }
} }
const viewportScrollbarVariant = customizationService.getCustomization('viewportScrollbar.variant');
const useProgressScrollbar = viewportScrollbarVariant !== 'legacy';
return ( return (
<div className="noselect"> <div className="noselect">
<ViewportImageScrollbar {useProgressScrollbar ? (
viewportId={viewportId} <ViewportSliceProgressScrollbar
viewportData={viewportData} viewportId={viewportId}
element={element} viewportData={viewportData}
imageSliceData={imageSliceData} element={element}
setImageSliceData={setImageSliceData} imageSliceData={imageSliceData}
scrollbarHeight={scrollbarHeight} setImageSliceData={setImageSliceData}
servicesManager={servicesManager} servicesManager={servicesManager}
/> />
) : (
<ViewportImageScrollbar
viewportId={viewportId}
viewportData={viewportData}
element={element}
imageSliceData={imageSliceData}
setImageSliceData={setImageSliceData}
scrollbarHeight={scrollbarHeight}
servicesManager={servicesManager}
/>
)}
<CustomizableViewportOverlay <CustomizableViewportOverlay
imageSliceData={imageSliceData} imageSliceData={imageSliceData}

View File

@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { vec3 } from 'gl-matrix'; import { vec3 } from 'gl-matrix';
import PropTypes from 'prop-types'; 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 { Enums as csToolsEnums, UltrasoundPleuraBLineTool } from '@cornerstonejs/tools';
import type { ImageSliceData } from '@cornerstonejs/core/types'; import type { ImageSliceData } from '@cornerstonejs/core/types';
import { ViewportOverlay, formatDICOMDate } from '@ohif/ui-next'; import { ViewportOverlay, formatDICOMDate } from '@ohif/ui-next';
@ -9,12 +9,14 @@ import type { InstanceMetadata } from '@ohif/core/src/types';
import { formatDICOMTime, formatNumberPrecision } from './utils'; import { formatDICOMTime, formatNumberPrecision } from './utils';
import { utils } from '@ohif/core'; import { utils } from '@ohif/core';
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService'; import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
import { getViewportAdapter } from '../../services/ViewportService/adapter';
import { getViewportDataShapeType } from '../../utils/viewportDataShape';
import './CustomizableViewportOverlay.css'; import './CustomizableViewportOverlay.css';
import { useViewportRendering } from '../../hooks'; import { useViewportRendering } from '../../hooks';
const EPSILON = 1e-4; const EPSILON = 1e-4;
const { formatPN } = utils; const { formatPN, formatValue } = utils;
type ViewportData = StackViewportData | VolumeViewportData; type ViewportData = StackViewportData | VolumeViewportData;
@ -67,10 +69,9 @@ function CustomizableViewportOverlay({
}) { }) {
const { cornerstoneViewportService, customizationService, toolGroupService, displaySetService } = const { cornerstoneViewportService, customizationService, toolGroupService, displaySetService } =
servicesManager.services; servicesManager.services;
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
const [scale, setScale] = useState(1); const [scale, setScale] = useState(1);
const [annotationState, setAnnotationState] = useState(0); const [annotationState, setAnnotationState] = useState(0);
const { isViewportBackgroundLight: isLight } = useViewportRendering(viewportId); const { isViewportBackgroundLight: isLight, windowLevel: voi } = useViewportRendering(viewportId);
const { imageIndex } = imageSliceData; const { imageIndex } = imageSliceData;
// Historical usage defined the overlays as separate items due to lack of // Historical usage defined the overlays as separate items due to lack of
@ -110,30 +111,6 @@ function CustomizableViewportOverlay({
}; };
}, [viewportData, viewportId, instanceNumber, cornerstoneViewportService]); }, [viewportData, viewportId, instanceNumber, cornerstoneViewportService]);
/**
* Updating the VOI when the viewport changes its voi
*/
useEffect(() => {
const updateVOI = eventDetail => {
const { range } = eventDetail.detail;
if (!range) {
return;
}
const { lower, upper } = range;
const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(lower, upper);
setVOI({ windowCenter, windowWidth });
};
element.addEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
return () => {
element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
};
}, [viewportId, viewportData, voi, element]);
const annotationModified = useCallback(evt => { const annotationModified = useCallback(evt => {
if (evt.detail.annotation.metadata.toolName === UltrasoundPleuraBLineTool.toolName) { if (evt.detail.annotation.metadata.toolName === UltrasoundPleuraBLineTool.toolName) {
// Update the annotation state to trigger a re-render // Update the annotation state to trigger a re-render
@ -209,7 +186,12 @@ function CustomizableViewportOverlay({
} else { } else {
const renderItem = customizationService.transform(item); const renderItem = customizationService.transform(item);
if (typeof renderItem.contentF === 'function') { if (
renderItem &&
typeof renderItem === 'object' &&
'contentF' in renderItem &&
typeof renderItem.contentF === 'function'
) {
return renderItem.contentF(overlayItemProps); return renderItem.contentF(overlayItemProps);
} }
} }
@ -226,6 +208,7 @@ function CustomizableViewportOverlay({
scale, scale,
instanceNumber, instanceNumber,
annotationState, annotationState,
isLight,
] ]
); );
@ -288,7 +271,7 @@ function getDisplaySets(viewportData, displaySetService) {
const getInstanceNumber = (viewportData, viewportId, imageIndex, cornerstoneViewportService) => { const getInstanceNumber = (viewportData, viewportId, imageIndex, cornerstoneViewportService) => {
let instanceNumber; let instanceNumber;
switch (viewportData.viewportType) { switch (getViewportDataShapeType(viewportData)) {
case Enums.ViewportType.STACK: case Enums.ViewportType.STACK:
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex); instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
break; break;
@ -355,8 +338,11 @@ function _getInstanceNumberFromVolume(
return; return;
} }
const camera = cornerstoneViewport.getCamera(); const viewPlaneNormal = getViewportAdapter(cornerstoneViewport).getViewPlaneNormal();
const { viewPlaneNormal } = camera;
if (!viewPlaneNormal) {
return;
}
// checking if camera is looking at the acquisition plane (defined by the direction on the volume) // checking if camera is looking at the acquisition plane (defined by the direction on the volume)
const scanAxisNormal = direction.slice(6, 9); const scanAxisNormal = direction.slice(6, 9);
@ -369,7 +355,10 @@ function _getInstanceNumberFromVolume(
const imageId = imageIds[imageIndex]; const imageId = imageIds[imageIndex];
if (!imageId) { if (!imageId) {
return {}; // No image at this index (e.g. a single-image volume scrolled out of
// range). Return undefined so the overlay falls back to the slice count
// instead of rendering an empty object as "[object Object]".
return;
} }
const { instanceNumber } = metaData.get('generalImageModule', imageId) || {}; const { instanceNumber } = metaData.get('generalImageModule', imageId) || {};
@ -381,7 +370,8 @@ function OverlayItem(props) {
const { instance, customization = {} } = props; const { instance, customization = {} } = props;
const { color, attribute, title, label, background } = customization; const { color, attribute, title, label, background } = customization;
const value = customization.contentF?.(props, customization) ?? instance?.[attribute]; const value = customization.contentF?.(props, customization) ?? instance?.[attribute];
if (value === undefined || value === null) { const displayValue = formatValue(value);
if (displayValue === null || displayValue === '') {
return null; return null;
} }
return ( return (
@ -391,7 +381,7 @@ function OverlayItem(props) {
title={title} title={title}
> >
{label ? <span className="mr-1 shrink-0">{label}</span> : null} {label ? <span className="mr-1 shrink-0">{label}</span> : null}
<span className="ml-0 mr-2 shrink-0">{value}</span> <span className="ml-0 shrink-0">{displayValue}</span>
</div> </div>
); );
} }
@ -402,6 +392,7 @@ function OverlayItem(props) {
*/ */
function VOIOverlayItem({ voi, customization }: OverlayItemProps) { function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
const { windowWidth, windowCenter } = voi; const { windowWidth, windowCenter } = voi;
const { title } = customization;
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') { if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
return null; return null;
} }
@ -410,6 +401,7 @@ function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
<div <div
className="overlay-item flex flex-row" className="overlay-item flex flex-row"
style={{ color: customization?.color }} style={{ color: customization?.color }}
title={title}
> >
<span className="mr-0.5 shrink-0 opacity-[0.70]">W:</span> <span className="mr-0.5 shrink-0 opacity-[0.70]">W:</span>
<span className="mr-2.5 shrink-0">{windowWidth.toFixed(0)}</span> <span className="mr-2.5 shrink-0">{windowWidth.toFixed(0)}</span>
@ -443,11 +435,13 @@ function InstanceNumberOverlayItem({
customization, customization,
}: OverlayItemProps) { }: OverlayItemProps) {
const { imageIndex, numberOfSlices } = imageSliceData; const { imageIndex, numberOfSlices } = imageSliceData;
const { title } = customization;
return ( return (
<div <div
className="overlay-item flex flex-row" className="overlay-item flex flex-row"
style={{ color: (customization && customization.color) || undefined }} style={{ color: (customization && customization.color) || undefined }}
title={title}
> >
<span> <span>
{instanceNumber !== undefined && instanceNumber !== null ? ( {instanceNumber !== undefined && instanceNumber !== null ? (

View File

@ -1,7 +1,9 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Enums, VolumeViewport3D, utilities as csUtils } from '@cornerstonejs/core'; import { utilities as csUtils } from '@cornerstonejs/core';
import { ImageScrollbar } from '@ohif/ui-next'; import { ImageScrollbar } from '@ohif/ui-next';
import { isVolume3DViewportType } from '../../utils/getLegacyViewportType';
import { getSliceEventName, getViewportSliceCount } from '../../utils/viewportDataShape';
function CornerstoneImageScrollbar({ function CornerstoneImageScrollbar({
viewportData, viewportData,
@ -40,16 +42,16 @@ function CornerstoneImageScrollbar({
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || viewport instanceof VolumeViewport3D) { if (!viewport || isVolume3DViewportType(viewport)) {
return; return;
} }
try { try {
const imageIndex = viewport.getCurrentImageIdIndex(); const imageIndex = viewport.getCurrentImageIdIndex();
const numberOfSlices = viewport.getNumberOfSlices(); const numberOfSlices = getViewportSliceCount(viewportData, viewport);
setImageSliceData({ setImageSliceData({
imageIndex: imageIndex, imageIndex,
numberOfSlices, numberOfSlices,
}); });
} catch (error) { } catch (error) {
@ -61,15 +63,11 @@ function CornerstoneImageScrollbar({
if (!viewportData) { if (!viewportData) {
return; return;
} }
const { viewportType } = viewportData; const eventId = getSliceEventName(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 updateIndex = event => { const updateIndex = event => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || viewport instanceof VolumeViewport3D) { if (!viewport || isVolume3DViewportType(viewport)) {
return; return;
} }
const { imageIndex, newImageIdIndex = imageIndex, imageIdIndex } = event.detail; const { imageIndex, newImageIdIndex = imageIndex, imageIdIndex } = event.detail;

View File

@ -6,6 +6,7 @@ import { vec3 } from 'gl-matrix';
import './ViewportOrientationMarkers.css'; import './ViewportOrientationMarkers.css';
import { useViewportRendering } from '../../hooks'; import { useViewportRendering } from '../../hooks';
import { getViewportDataShapeType } from '../../utils/viewportDataShape';
const { getOrientationStringLPS, invertOrientationStringLPS } = utilities.orientation; const { getOrientationStringLPS, invertOrientationStringLPS } = utilities.orientation;
function ViewportOrientationMarkers({ function ViewportOrientationMarkers({
@ -46,7 +47,9 @@ function ViewportOrientationMarkers({
return ''; 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 imageIndex = imageSliceData.imageIndex;
const imageId = viewportData.data[0].imageIds?.[imageIndex]; const imageId = viewportData.data[0].imageIds?.[imageIndex];

View File

@ -0,0 +1,201 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { utilities as csUtils } from '@cornerstonejs/core';
import { isVolume3DViewportType } from '../../../utils/getLegacyViewportType';
import {
SmartScrollbar,
SmartScrollbarTrack,
SmartScrollbarFill,
SmartScrollbarIndicator,
SmartScrollbarEndpoints,
} from '@ohif/ui-next';
import { getViewportImageIds } from './helpers';
import {
useLoadedSliceBytes,
useProgressScrollbarMode,
useViewedSliceBytes,
useViewportSliceSync,
} from './hooks';
import { ViewportSliceProgressScrollbarProps } from './types';
function ViewportSliceProgressScrollbar({
viewportData,
viewportId,
element,
imageSliceData,
setImageSliceData,
servicesManager,
}: ViewportSliceProgressScrollbarProps) {
const { cineService, cornerstoneViewportService, customizationService, viewedDataService } =
servicesManager.services;
const showLoadedEndpoints =
customizationService.getCustomization('viewportScrollbar.showLoadedEndpoints') !== false;
const showLoadedFill =
customizationService.getCustomization('viewportScrollbar.showLoadedFill') !== false;
const showViewedFill =
customizationService.getCustomization('viewportScrollbar.showViewedFill') !== false;
const showLoadingPattern =
customizationService.getCustomization('viewportScrollbar.showLoadingPattern') !== false;
const viewedDwellMsRaw = customizationService.getCustomization('viewportScrollbar.viewedDwellMs');
const loadedBatchIntervalMsRaw = customizationService.getCustomization(
'viewportScrollbar.loadedBatchIntervalMs'
);
const viewedDwellMs =
typeof viewedDwellMsRaw === 'number' && viewedDwellMsRaw >= 0 ? viewedDwellMsRaw : 0;
const loadedBatchIntervalMs =
typeof loadedBatchIntervalMsRaw === 'number' && loadedBatchIntervalMsRaw >= 0
? loadedBatchIntervalMsRaw
: 200;
const { numberOfSlices, imageIndex } = imageSliceData;
const imageIds = useMemo(() => getViewportImageIds(viewportData), [viewportData]);
const imageIdToIndex = useMemo(() => {
const map = new Map<string, number>();
for (let i = 0; i < imageIds.length; i++) {
const imageId = imageIds[i];
if (imageId) {
map.set(imageId, i);
}
}
return map;
}, [imageIds]);
const isFullMode = useProgressScrollbarMode({
viewportData,
viewportId,
element,
cornerstoneViewportService,
});
useViewportSliceSync({
viewportData,
viewportId,
element,
cornerstoneViewportService,
setImageSliceData,
});
const {
bytes: loadedBytes,
version: loadedVersion,
isFull: isFullyLoaded,
} = useLoadedSliceBytes({
isFullMode,
numberOfSlices,
viewportData,
imageIds,
imageIdToIndex,
loadedBatchIntervalMs,
});
const { bytes: viewedBytes, version: viewedVersion } = useViewedSliceBytes({
isFullMode,
numberOfSlices,
imageIndex,
imageIds,
imageIdToIndex,
viewedDwellMs,
viewedDataService,
});
const onScrollbarValueChange = targetImageIndex => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || isVolume3DViewportType(viewport)) {
return;
}
const { isCineEnabled } = cineService.getState();
if (isCineEnabled) {
cineService.stopClip(element, { viewportId });
cineService.setCine({ id: viewportId, frameRate: undefined, isPlaying: false });
}
csUtils.jumpToSlice(viewport.element, {
imageIndex: targetImageIndex,
debounceLoading: true,
});
};
const isLoading = isFullMode && showLoadingPattern ? !isFullyLoaded : false;
if (!numberOfSlices || numberOfSlices <= 1) {
return null;
}
return (
<div
style={{
position: 'absolute',
right: 0,
top: 0,
height: '100%',
padding: '8px 5px',
zIndex: 10,
}}
>
<div
style={{
position: 'relative',
height: '100%',
width: '11px',
}}
>
<SmartScrollbar
className="absolute inset-0"
value={imageIndex || 0}
total={numberOfSlices}
onValueChange={onScrollbarValueChange}
isLoading={isLoading}
enableKeyboardNavigation={false}
aria-label="Image navigation scrollbar"
indicator={
customizationService.getCustomization('viewportScrollbar.indicator') as
| Record<string, unknown>
| undefined
}
>
<SmartScrollbarTrack>
{isFullMode && showLoadedFill && (
<SmartScrollbarFill
marked={loadedBytes}
version={loadedVersion}
className="bg-neutral/25"
loadingClassName="bg-neutral/50"
/>
)}
{isFullMode && showViewedFill && (
<SmartScrollbarFill
marked={viewedBytes}
version={viewedVersion}
className="bg-primary/35"
loadingClassName="bg-primary/35"
/>
)}
</SmartScrollbarTrack>
<SmartScrollbarIndicator />
{isFullMode && showLoadedEndpoints && (
<SmartScrollbarEndpoints
marked={loadedBytes}
version={loadedVersion}
/>
)}
</SmartScrollbar>
</div>
</div>
);
}
ViewportSliceProgressScrollbar.propTypes = {
viewportData: PropTypes.object,
viewportId: PropTypes.string.isRequired,
element: PropTypes.instanceOf(Element),
imageSliceData: PropTypes.object.isRequired,
setImageSliceData: PropTypes.func.isRequired,
servicesManager: PropTypes.object.isRequired,
};
export default ViewportSliceProgressScrollbar;

View File

@ -0,0 +1,46 @@
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;
return newImageIdIndex ?? imageIdIndex;
}
export function getViewportImageIds(viewportData: ViewportData): string[] {
if (!viewportData?.data?.length) {
return [];
}
const firstData = viewportData.data[0];
const volumeImageIds = (firstData as any).volume?.imageIds as string[] | undefined;
const datumImageIds = (firstData as any).imageIds as string[] | undefined;
return volumeImageIds || datumImageIds || [];
}
export function isProgressFullMode(viewportData: ViewportData, viewport): boolean {
if (!viewportData || !viewport || isVolume3DViewportType(viewport)) {
return false;
}
// 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 (shape === 'volume') {
return adapter.isInAcquisitionPlane();
}
return false;
}
export function getImageIdFromCacheEvent(event): string | undefined {
const detail = event?.detail;
return detail?.imageId || detail?.image?.imageId || detail?.cachedImage?.imageId;
}

View File

@ -0,0 +1,377 @@
import { useEffect, useRef, useState } from 'react';
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';
export function useProgressScrollbarMode({
viewportData,
viewportId,
element,
cornerstoneViewportService,
}: {
viewportData: ViewportData;
viewportId: string;
element: HTMLElement;
cornerstoneViewportService: AppTypes.CornerstoneViewportService;
}) {
const [isFullMode, setIsFullMode] = useState(false);
const lastViewPlaneNormalRef = useRef<number[] | null>(null);
/**
* Tracks whether this viewport should render full progress UI (stack or acquisition-plane
* orthographic volume) versus minimal UI. We compute once on setup and recompute on each
* CAMERA_MODIFIED event so stack->MPR transitions and acquisition-plane changes are reflected
* immediately.
*/
useEffect(() => {
if (!viewportData) {
return;
}
const updateMode = () => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const viewportImageData = viewport?.getImageData?.();
const nextViewPlaneNormal = viewport?.getCamera?.()?.viewPlaneNormal as number[] | undefined;
// Do not update the lastViewPlaneNormalRef until we have a valid viewportImageData.
// Without viewportImageData, the viewport is not fully initialized and the isAcquisitionPlane
// check will not be accurate.
if (viewportImageData && nextViewPlaneNormal) {
lastViewPlaneNormalRef.current = [...nextViewPlaneNormal];
}
const nextMode = isProgressFullMode(viewportData, viewport);
setIsFullMode(prevMode => (prevMode === nextMode ? prevMode : nextMode));
};
updateMode();
const onCameraModified = () => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const nextViewPlaneNormal = viewport?.getCamera?.()?.viewPlaneNormal as number[] | undefined;
const previousViewPlaneNormal = lastViewPlaneNormalRef.current;
// Ignore camera updates that keep the same orientation (pan/zoom/scroll).
if (nextViewPlaneNormal && previousViewPlaneNormal) {
if (utilities.isEqual(nextViewPlaneNormal, previousViewPlaneNormal)) {
return;
}
}
updateMode();
};
element.addEventListener(Enums.Events.CAMERA_MODIFIED, onCameraModified);
return () => {
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, onCameraModified);
};
}, [viewportData, viewportId, cornerstoneViewportService, element]);
return isFullMode;
}
export function useViewportSliceSync({
viewportData,
viewportId,
element,
cornerstoneViewportService,
setImageSliceData,
}: {
viewportData: ViewportData;
viewportId: string;
element: HTMLElement;
cornerstoneViewportService: AppTypes.CornerstoneViewportService;
setImageSliceData: (data: ImageSliceData) => void;
}) {
/**
* Keeps shared slice state in sync: first initialize from the live viewport snapshot, then
* subscribe to navigation/render events for incremental updates while users scroll.
*/
useEffect(() => {
if (!viewportData) {
return;
}
// 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 = getViewportSliceCount(viewportData, viewport);
pushSliceData(currentImageIndex, currentNumberOfSlices);
} catch (error) {
console.warn(error);
}
};
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);
if (!viewport || isVolume3DViewportType(viewport)) {
return;
}
const nextImageIndex = getImageIndexFromEvent(event);
if (nextImageIndex == null) {
return;
}
const nextNumberOfSlices = viewport.getNumberOfSlices();
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]);
}
export function useLoadedSliceBytes({
isFullMode,
numberOfSlices,
viewportData,
imageIds,
imageIdToIndex,
loadedBatchIntervalMs,
}: {
isFullMode: boolean;
numberOfSlices: number;
viewportData: ViewportData;
imageIds: string[];
imageIdToIndex: Map<string, number>;
loadedBatchIntervalMs: number;
}) {
const loadedState = useByteArray(numberOfSlices || 0, loadedBatchIntervalMs);
const {
resetWith: resetLoaded,
setByte: setLoadedByte,
clearByte: clearLoadedByte,
} = loadedState;
/**
* Keeps the loaded byte array in sync with Cornerstone cache: seed from cache whenever stack /
* mode / slice count changes, then subscribe so cache add/remove updates stay incremental.
* Seeding runs immediately before registering listeners in the same effect.
*/
useEffect(() => {
if (isFullMode && numberOfSlices) {
resetLoaded(bytes => {
for (let i = 0; i < bytes.length; i++) {
const imageId = imageIds[i];
if (imageId && cornerstoneCache.isLoaded(imageId)) {
bytes[i] = 1;
}
}
});
}
if (!isFullMode || !viewportData) {
return;
}
const markLoaded = event => {
const imageId = getImageIdFromCacheEvent(event);
if (!imageId) {
return;
}
const index = imageIdToIndex.get(imageId);
if (index !== undefined) {
setLoadedByte(index);
}
};
const markRemoved = event => {
const imageId = getImageIdFromCacheEvent(event);
if (!imageId) {
return;
}
const index = imageIdToIndex.get(imageId);
if (index !== undefined) {
clearLoadedByte(index);
}
};
eventTarget.addEventListener(Enums.Events.IMAGE_CACHE_IMAGE_ADDED, markLoaded);
eventTarget.addEventListener(Enums.Events.IMAGE_CACHE_IMAGE_REMOVED, markRemoved);
return () => {
eventTarget.removeEventListener(Enums.Events.IMAGE_CACHE_IMAGE_ADDED, markLoaded);
eventTarget.removeEventListener(Enums.Events.IMAGE_CACHE_IMAGE_REMOVED, markRemoved);
};
}, [
imageIds,
isFullMode,
numberOfSlices,
viewportData,
imageIdToIndex,
resetLoaded,
setLoadedByte,
clearLoadedByte,
]);
return loadedState;
}
export function useViewedSliceBytes({
isFullMode,
numberOfSlices,
imageIndex,
imageIds,
imageIdToIndex,
viewedDwellMs,
viewedDataService,
}: {
isFullMode: boolean;
numberOfSlices: number;
imageIndex: number;
imageIds: string[];
imageIdToIndex: Map<string, number>;
viewedDwellMs: number;
viewedDataService: AppTypes.ViewedDataService;
}) {
const viewedState = useByteArray(numberOfSlices || 0);
const { resetWith: resetViewed, setByte: setViewedByte } = viewedState;
/**
* Keeps the viewed byte array in sync with the global viewed-data store: seed from the store
* whenever stack / mode / slice count changes, then subscribe so `markDataViewed` updates stay
* incremental. Seeding runs immediately before registering the listener in the same effect.
*/
useEffect(() => {
if (isFullMode && numberOfSlices) {
resetViewed(bytes => {
for (let i = 0; i < bytes.length; i++) {
const imageId = imageIds[i];
if (imageId && viewedDataService?.isDataViewed(imageId)) {
bytes[i] = 1;
}
}
});
}
if (!viewedDataService) {
return;
}
const subscription = viewedDataService.subscribeViewedDataChanges(
({
viewedDataId,
viewedDataCleared,
}: {
viewedDataId?: string;
viewedDataCleared?: boolean;
}) => {
if (!isFullMode || !numberOfSlices) {
return;
}
if (viewedDataCleared) {
resetViewed(bytes => {
bytes.fill(0);
});
return;
}
const index = imageIdToIndex.get(viewedDataId);
if (index !== undefined) {
setViewedByte(index);
}
}
);
return () => {
subscription.unsubscribe();
};
}, [
imageIds,
isFullMode,
numberOfSlices,
imageIdToIndex,
resetViewed,
setViewedByte,
viewedDataService,
]);
/**
* Marks slices as viewed in full mode. With `viewedDwellMs === 0`, marking is immediate on
* index change; otherwise a dwell timer is used and cleaned up on subsequent changes/unmount.
*/
useEffect(() => {
if (!isFullMode || !numberOfSlices) {
return;
}
const markViewed = (targetIndex: number) => {
setViewedByte(targetIndex);
const imageId = imageIds[targetIndex];
if (imageId) {
viewedDataService?.markDataViewed(imageId);
}
};
if (viewedDwellMs === 0) {
markViewed(imageIndex || 0);
return;
}
const timerId = window.setTimeout(() => {
markViewed(imageIndex || 0);
}, viewedDwellMs);
return () => {
window.clearTimeout(timerId);
};
}, [
isFullMode,
numberOfSlices,
imageIndex,
imageIds,
setViewedByte,
viewedDwellMs,
viewedDataService,
]);
return viewedState;
}

View File

@ -0,0 +1 @@
export { default } from './ViewportSliceProgressScrollbar';

View File

@ -0,0 +1,17 @@
import { StackViewportData, VolumeViewportData } from '../../../types/CornerstoneCacheService';
export type ViewportData = StackViewportData | VolumeViewportData;
export type ImageSliceData = {
imageIndex: number;
numberOfSlices: number;
};
export type ViewportSliceProgressScrollbarProps = {
viewportData: ViewportData | null;
viewportId: string;
element: HTMLElement;
imageSliceData: ImageSliceData;
setImageSliceData: (data: ImageSliceData) => void;
servicesManager: AppTypes.ServicesManager;
};

View File

@ -1,11 +1,7 @@
import { import {
getEnabledElement, getEnabledElement,
StackViewport,
VolumeViewport,
utilities as csUtils, utilities as csUtils,
Enums as CoreEnums,
Types as CoreTypes, Types as CoreTypes,
BaseVolumeViewport,
getRenderingEngines, getRenderingEngines,
} from '@cornerstonejs/core'; } from '@cornerstonejs/core';
import { import {
@ -15,6 +11,7 @@ import {
annotation, annotation,
Types as ToolTypes, Types as ToolTypes,
SplineContourSegmentationTool, SplineContourSegmentationTool,
cancelActiveManipulations,
} from '@cornerstonejs/tools'; } from '@cornerstonejs/tools';
import { import {
SegmentInfo, SegmentInfo,
@ -32,12 +29,22 @@ import {
colorPickerDialog, colorPickerDialog,
callInputDialog, callInputDialog,
} from '@ohif/extension-default'; } from '@ohif/extension-default';
import { vec3, mat4 } from 'gl-matrix';
import toggleImageSliceSync from './utils/imageSliceSync/toggleImageSliceSync'; import toggleImageSliceSync from './utils/imageSliceSync/toggleImageSliceSync';
// Sanctioned flag read: RTSTRUCT contour hydration pins the referenced image to
// stack mode on the native ("next") path, a decision made before a target viewport exists.
import { getHydrationViewportTypeForModality } from './utils/nextViewportPolicies';
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection'; import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
import { getViewportEnabledElement } from './utils/getViewportEnabledElement'; import { getViewportEnabledElement } from './utils/getViewportEnabledElement';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement'; import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
import toggleVOISliceSync from './utils/toggleVOISliceSync'; import toggleVOISliceSync from './utils/toggleVOISliceSync';
import {
isStackViewportType,
isOrthographicViewportType,
isVolume3DViewportType,
isVolumeViewportType,
} from './utils/getLegacyViewportType';
import { viewportOperations as ops } from './services/ViewportService/backends/viewportOperations';
import { getViewportAdapter } from './services/ViewportService/adapter';
import { import {
usePositionPresentationStore, usePositionPresentationStore,
useSegmentationPresentationStore, useSegmentationPresentationStore,
@ -49,8 +56,6 @@ import { updateSegmentBidirectionalStats } from './utils/updateSegmentationStats
import { generateSegmentationCSVReport } from './utils/generateSegmentationCSVReport'; import { generateSegmentationCSVReport } from './utils/generateSegmentationCSVReport';
import { getUpdatedViewportsForSegmentation } from './utils/hydrationUtils'; import { getUpdatedViewportsForSegmentation } from './utils/hydrationUtils';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums'; import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
import { isMeasurementWithinViewport } from './utils/isMeasurementWithinViewport';
import { getCenterExtent } from './utils/getCenterExtent';
import { EasingFunctionEnum } from './utils/transitions'; import { EasingFunctionEnum } from './utils/transitions';
import { createSegmentationForViewport } from './utils/createSegmentationForViewport'; import { createSegmentationForViewport } from './utils/createSegmentationForViewport';
import { utilities as segmentationUtilities } from '@cornerstonejs/tools/segmentation'; import { utilities as segmentationUtilities } from '@cornerstonejs/tools/segmentation';
@ -143,9 +148,32 @@ function commandsModule({
return getViewportEnabledElement(viewportId); return getViewportEnabledElement(viewportId);
} }
// Resolves the cornerstone viewport for a command: the given viewport id, else the
// active one. Returns undefined when nothing is enabled.
function _resolveViewport(viewportId?: string) {
const enabledElement = viewportId
? _getViewportEnabledElement(viewportId)
: _getActiveViewportEnabledElement();
return enabledElement?.viewport;
}
function _getActiveViewportToolGroupId() { function _getActiveViewportToolGroupId() {
const viewport = _getActiveViewportEnabledElement(); const viewport = _getActiveViewportEnabledElement();
return toolGroupService.getToolGroupForViewport(viewport.id); const toolGroup = viewport && toolGroupService.getToolGroupForViewport(viewport.id);
return toolGroup?.id;
}
function _usesPrimaryActivation(bindings) {
if (!bindings?.length) {
return true;
}
return bindings.some(
binding =>
binding.mouseButton === Enums.MouseBindings.Primary &&
binding.modifierKey == null &&
binding.numTouchPoints == null
);
} }
function _getActiveSegmentationInfo() { function _getActiveSegmentationInfo() {
@ -221,23 +249,11 @@ function commandsModule({
viewport.setViewReference(metadata); viewport.setViewReference(metadata);
viewport.render(); viewport.render();
/** // If the measurement is not visible inside the current viewport, move the
* If the measurement is not visible inside the current viewport, // camera to it. The operations backend handles the lane: legacy re-centers
* we need to move the camera to the measurement. // in-plane (getCamera/setCamera), native skips it (no in-plane pan yet, CS-14)
*/ // since setViewReference above already navigated to the measurement's slice.
if (!isMeasurementWithinViewport(viewport, measurement)) { if (ops.centerOnMeasurement(viewport, measurement)) {
const camera = viewport.getCamera();
const { focalPoint: cameraFocalPoint, position: cameraPosition } = camera;
const { center, extent } = getCenterExtent(measurement);
const position = vec3.sub(vec3.create(), cameraPosition, cameraFocalPoint);
vec3.add(position, position, center);
viewport.setCamera({ focalPoint: center, position: position as any });
/** Zoom out if the measurement is too large */
const measurementSize = vec3.dist(extent.min, extent.max);
if (measurementSize > camera.parallelScale) {
const scaleFactor = measurementSize / camera.parallelScale;
viewport.setZoom(viewport.getZoom() / scaleFactor);
}
viewport.render(); viewport.render();
} }
@ -290,6 +306,20 @@ function commandsModule({
commandsManager.run('setDisplaySetsForViewports', { viewportsToUpdate: updatedViewports }); commandsManager.run('setDisplaySetsForViewports', { viewportsToUpdate: updatedViewports });
}, },
/**
* Cancels any in-progress annotation manipulation (e.g. drawing a Spline,
* Livewire or PlanarFreehand contour) on the active viewport. Reached on
* Escape via the `cancelActiveOperation` command. `cancelActiveManipulations`
* invokes the `cancel` method of each active/passive tool that has an
* in-progress annotation, so it is a no-op when nothing is being drawn.
*/
cancelMeasurement: () => {
const element = _getActiveViewportEnabledElement()?.viewport?.element;
if (element) {
cancelActiveManipulations(element);
}
},
hydrateSecondaryDisplaySet: async ({ displaySet, viewportId }) => { hydrateSecondaryDisplaySet: async ({ displaySet, viewportId }) => {
if (!displaySet) { if (!displaySet) {
return; return;
@ -310,7 +340,7 @@ function commandsModule({
// Todo: check if PMAP modality should be handled such as SEG // Todo: check if PMAP modality should be handled such as SEG
displaySet.Modality !== 'SEG' displaySet.Modality !== 'SEG'
? SegmentationRepresentations.Contour ? SegmentationRepresentations.Contour
: viewport.type === CoreEnums.ViewportType.VOLUME_3D : isVolume3DViewportType(viewport)
? SegmentationRepresentations.Surface ? SegmentationRepresentations.Surface
: SegmentationRepresentations.Labelmap; : SegmentationRepresentations.Labelmap;
@ -341,6 +371,9 @@ function commandsModule({
const results = commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', { const results = commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
viewportId, viewportId,
displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID], displaySetInstanceUIDs: [referencedDisplaySet.displaySetInstanceUID],
// RTSTRUCT-on-next pins the referenced image to stack mode on hydrate;
// see the policy's rationale in utils/nextViewportPolicies.
viewportType: getHydrationViewportTypeForModality(displaySet.Modality),
}); });
const disableEditing = customizationService.getCustomization( const disableEditing = customizationService.getCustomization(
@ -398,12 +431,31 @@ function commandsModule({
const { segmentationId: targetId, segmentIndex: targetIndex } = targetSegmentation; const { segmentationId: targetId, segmentIndex: targetIndex } = targetSegmentation;
// Check if the segment has voxels before computing bidirectional measurement
const uniqueSegmentIndices = cstUtils.segmentation.getUniqueSegmentIndices(targetId);
const hasVoxels = uniqueSegmentIndices.includes(targetIndex);
if (!hasVoxels) {
uiNotificationService.show({
title: i18n.t('SegmentationPanel:Segment Bidirectional'),
message: i18n.t(
'SegmentationPanel:Draw a segment before using bidirectional measurement'
),
type: 'warning',
});
return;
}
// Get bidirectional measurement data // Get bidirectional measurement data
const bidirectionalData = await cstUtils.segmentation.getSegmentLargestBidirectional({ const bidirectionalData = await cstUtils.segmentation.getSegmentLargestBidirectional({
segmentationId: targetId, segmentationId: targetId,
segmentIndices: [targetIndex], segmentIndices: [targetIndex],
}); });
if (!bidirectionalData.length) {
return;
}
const activeViewportId = viewportGridService.getActiveViewportId(); const activeViewportId = viewportGridService.getActiveViewportId();
// Process each bidirectional measurement // Process each bidirectional measurement
@ -445,7 +497,7 @@ function commandsModule({
measurement => measurement.segmentIndex === targetIndex measurement => measurement.segmentIndex === targetIndex
); );
commandsManager.run('jumpToMeasurement', { commandsManager.run('jumpToMeasurement', {
uid: activeBidirectional.annotationUID, uid: activeBidirectional?.annotationUID,
}); });
}, },
interpolateLabelmap: () => { interpolateLabelmap: () => {
@ -737,6 +789,9 @@ function commandsModule({
* Also marks any provided display measurements isActive value * Also marks any provided display measurements isActive value
*/ */
jumpToMeasurement: ({ uid, displayMeasurements = [] }) => { jumpToMeasurement: ({ uid, displayMeasurements = [] }) => {
if (!uid) {
return;
}
measurementService.jumpToMeasurement(viewportGridService.getActiveViewportId(), uid); measurementService.jumpToMeasurement(viewportGridService.getActiveViewportId(), uid);
for (const measurement of displayMeasurements) { for (const measurement of displayMeasurements) {
measurement.isActive = measurement.uid === uid; measurement.isActive = measurement.uid === uid;
@ -868,34 +923,28 @@ function commandsModule({
const windowWidthNum = Number(windowWidth); const windowWidthNum = Number(windowWidth);
const windowCenterNum = Number(windowCenter); const windowCenterNum = Number(windowCenter);
// get actor from the viewport
const renderingEngine = cornerstoneViewportService.getRenderingEngine(); const renderingEngine = cornerstoneViewportService.getRenderingEngine();
const viewport = renderingEngine.getViewport(viewportId); const viewport = renderingEngine.getViewport(viewportId);
const { lower, upper } = csUtils.windowLevel.toLowHighRange(windowWidthNum, windowCenterNum); // Stale/invalid viewport ids resolve to undefined; bail out before the VOI
// apply + render below would throw.
if (viewport instanceof BaseVolumeViewport) { if (!viewport) {
const volumeId = actions.getVolumeIdForDisplaySet({ return;
viewportId,
displaySetInstanceUID,
});
viewport.setProperties(
{
voiRange: {
upper,
lower,
},
},
volumeId
);
} else {
viewport.setProperties({
voiRange: {
upper,
lower,
},
});
} }
// Legacy volume viewports target a specific volume; the command owns that
// resolution (it needs the service). The operations backend applies the VOI
// (legacy setProperties vs native setDisplaySetPresentation on the active binding).
const volumeId = isVolumeViewportType(viewport)
? actions.getVolumeIdForDisplaySet({ viewportId, displaySetInstanceUID })
: undefined;
ops.setWindowLevel(viewport, {
windowWidth: windowWidthNum,
windowCenter: windowCenterNum,
volumeId,
displaySetInstanceUID,
});
viewport.render(); viewport.render();
}, },
toggleViewportColorbar: ({ viewportId, displaySetInstanceUIDs, options = {} }) => { toggleViewportColorbar: ({ viewportId, displaySetInstanceUIDs, options = {} }) => {
@ -950,8 +999,12 @@ function commandsModule({
}, },
getVolumeIdForDisplaySet: ({ viewportId, displaySetInstanceUID }) => { getVolumeIdForDisplaySet: ({ viewportId, displaySetInstanceUID }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (viewport instanceof BaseVolumeViewport) { // `instanceof BaseVolumeViewport` is false for GenericViewport compat adapters,
const volumeIds = viewport.getAllVolumeIds(); // so use the capability guard instead (cornerstone 5.x generic-viewport guide).
// A stack-mode adapter passes the guard but getAllVolumeIds() returns [], so the
// result is the same null as before on stacks.
if (csUtils.viewportSupportsVolumeId(viewport)) {
const volumeIds = (viewport as CoreTypes.IVolumeViewport).getAllVolumeIds();
const volumeId = volumeIds.find(id => id.includes(displaySetInstanceUID)); const volumeId = volumeIds.find(id => id.includes(displaySetInstanceUID));
return volumeId; return volumeId;
} }
@ -995,32 +1048,48 @@ function commandsModule({
toolIsEnabled ? toolGroup.setToolDisabled(toolName) : toolGroup.setToolEnabled(toolName); toolIsEnabled ? toolGroup.setToolDisabled(toolName) : toolGroup.setToolEnabled(toolName);
}, },
toggleActiveDisabledToolbar({ value, itemId, toolGroupId }) { toggleActiveDisabledToolbar({ value, itemId, toolGroupId, toolGroupIds }) {
const toolName = itemId || value; const toolName = itemId || value;
toolGroupId = toolGroupId ?? _getActiveViewportToolGroupId(); const resolvedToolGroupIds = toolGroupIds?.length
const toolGroup = toolGroupService.getToolGroup(toolGroupId); ? toolGroupIds
if (!toolGroup || !toolGroup.hasTool(toolName)) { : [toolGroupId ?? _getActiveViewportToolGroupId()];
return;
}
const toolIsActive = [ resolvedToolGroupIds.forEach(toolGroupId => {
Enums.ToolModes.Active, const toolGroup = toolGroupService.getToolGroup(toolGroupId);
Enums.ToolModes.Enabled, if (!toolGroup || !toolGroup.hasTool(toolName)) {
Enums.ToolModes.Passive, return;
].includes(toolGroup.getToolOptions(toolName).mode);
toolIsActive
? toolGroup.setToolDisabled(toolName)
: actions.setToolActive({ toolName, toolGroupId });
// we should set the previously active tool to active after we set the
// current tool disabled
if (toolIsActive) {
const prevToolName = toolGroup.getPrevActivePrimaryToolName();
if (prevToolName !== toolName) {
actions.setToolActive({ toolName: prevToolName, toolGroupId });
} }
}
const toolIsActive = [
Enums.ToolModes.Active,
Enums.ToolModes.Enabled,
Enums.ToolModes.Passive,
].includes(toolGroup.getToolOptions(toolName).mode);
if (toolIsActive) {
toolGroup.setToolDisabled(toolName);
const bindings = toolGroupService.getToolBindings(toolGroupId, toolName);
if (_usesPrimaryActivation(bindings)) {
// we should set the previously active tool to active after we set the
// current tool disabled
const prevToolName = toolGroup.getPrevActivePrimaryToolName();
if (prevToolName !== toolName) {
actions.setToolActive({ toolName: prevToolName, toolGroupId });
}
}
return;
}
const bindings = toolGroupService.getToolBindings(toolGroupId, toolName);
if (_usesPrimaryActivation(bindings)) {
actions.setToolActive({ toolName, toolGroupId, bindings });
} else {
toolGroup.setToolActive(toolName, { bindings });
}
});
}, },
setToolActiveToolbar: ({ value, itemId, toolName, toolGroupIds = [], bindings }) => { setToolActiveToolbar: ({ value, itemId, toolName, toolGroupIds = [], bindings }) => {
// Sometimes it is passed as value (tools with options), sometimes as itemId (toolbar buttons) // Sometimes it is passed as value (tools with options), sometimes as itemId (toolbar buttons)
@ -1114,25 +1183,11 @@ function commandsModule({
viewportId?: string; viewportId?: string;
newValue?: 'toggle' | boolean; newValue?: 'toggle' | boolean;
}) => { }) => {
const enabledElement = viewportId const viewport = _resolveViewport(viewportId);
? _getViewportEnabledElement(viewportId) if (!viewport) {
: _getActiveViewportEnabledElement();
if (!enabledElement) {
return; return;
} }
ops.flipHorizontal(viewport, newValue);
const { viewport } = enabledElement;
let flipHorizontal: boolean;
if (newValue === 'toggle') {
const { flipHorizontal: currentHorizontalFlip } = viewport.getCamera();
flipHorizontal = !currentHorizontalFlip;
} else {
flipHorizontal = newValue;
}
viewport.setCamera({ flipHorizontal });
viewport.render(); viewport.render();
}, },
flipViewportVertical: ({ flipViewportVertical: ({
@ -1142,78 +1197,36 @@ function commandsModule({
viewportId?: string; viewportId?: string;
newValue?: 'toggle' | boolean; newValue?: 'toggle' | boolean;
}) => { }) => {
const enabledElement = viewportId const viewport = _resolveViewport(viewportId);
? _getViewportEnabledElement(viewportId) if (!viewport) {
: _getActiveViewportEnabledElement();
if (!enabledElement) {
return; return;
} }
ops.flipVertical(viewport, newValue);
const { viewport } = enabledElement;
let flipVertical: boolean;
if (newValue === 'toggle') {
const { flipVertical: currentVerticalFlip } = viewport.getCamera();
flipVertical = !currentVerticalFlip;
} else {
flipVertical = newValue;
}
viewport.setCamera({ flipVertical });
viewport.render(); viewport.render();
}, },
invertViewport: ({ element }) => { invertViewport: ({ element }) => {
let enabledElement; const viewport = element === undefined ? _resolveViewport() : element.viewport;
if (!viewport) {
if (element === undefined) {
enabledElement = _getActiveViewportEnabledElement();
} else {
enabledElement = element;
}
if (!enabledElement) {
return; return;
} }
ops.invert(viewport);
const { viewport } = enabledElement;
const { invert } = viewport.getProperties();
viewport.setProperties({ invert: !invert });
viewport.render(); viewport.render();
}, },
resetViewport: () => { resetViewport: () => {
const enabledElement = _getActiveViewportEnabledElement(); const viewport = _resolveViewport();
if (!viewport) {
if (!enabledElement) {
return; return;
} }
ops.reset(viewport);
const { viewport } = enabledElement;
viewport.resetProperties?.();
viewport.resetCamera();
viewport.render(); viewport.render();
}, },
scaleViewport: ({ direction }) => { scaleViewport: ({ direction }) => {
const enabledElement = _getActiveViewportEnabledElement(); const viewport = _resolveViewport();
const scaleFactor = direction > 0 ? 0.9 : 1.1; if (!viewport) {
if (!enabledElement) {
return; return;
} }
const { viewport } = enabledElement; ops.scaleBy(viewport, direction);
viewport.render();
if (viewport instanceof StackViewport) {
if (direction) {
const { parallelScale } = viewport.getCamera();
viewport.setCamera({ parallelScale: parallelScale * scaleFactor });
viewport.render();
} else {
viewport.resetCamera();
viewport.render();
}
}
}, },
/** Jumps the active viewport or the specified one to the given slice index */ /** Jumps the active viewport or the specified one to the given slice index */
@ -1234,9 +1247,9 @@ function commandsModule({
// -> Copied from cornerstone3D jumpToSlice\_getImageSliceData() // -> Copied from cornerstone3D jumpToSlice\_getImageSliceData()
let numberOfSlices = 0; let numberOfSlices = 0;
if (viewport instanceof StackViewport) { if (isStackViewportType(viewport)) {
numberOfSlices = viewport.getImageIds().length; numberOfSlices = viewport.getImageIds().length;
} else if (viewport instanceof VolumeViewport) { } else if (isOrthographicViewportType(viewport)) {
numberOfSlices = csUtils.getImageSliceDataForVolumeViewport(viewport).numberOfSlices; numberOfSlices = csUtils.getImageSliceDataForVolumeViewport(viewport).numberOfSlices;
} else { } else {
throw new Error('Unsupported viewport type'); throw new Error('Unsupported viewport type');
@ -1292,24 +1305,15 @@ function commandsModule({
// HP takes priority over the default opacity // HP takes priority over the default opacity
colormap = { ...colormap, opacity: hpOpacity || opacity }; colormap = { ...colormap, opacity: hpOpacity || opacity };
if (viewport instanceof StackViewport) { // The legacy orthographic branch resolves the volumeId from the display set;
viewport.setProperties({ colormap }); // fall back to the viewport's first display set (needs viewportGridService, so
// it is resolved here in the command rather than in the operations backend).
if (isOrthographicViewportType(viewport) && !displaySetInstanceUID) {
const { viewports } = viewportGridService.getState();
displaySetInstanceUID = viewports.get(viewportId)?.displaySetInstanceUIDs[0];
} }
if (viewport instanceof VolumeViewport) { ops.setColormap(viewport, { colormap, displaySetInstanceUID });
if (!displaySetInstanceUID) {
const { viewports } = viewportGridService.getState();
displaySetInstanceUID = viewports.get(viewportId)?.displaySetInstanceUIDs[0];
}
// ToDo: Find a better way of obtaining the volumeId that corresponds to the displaySetInstanceUID
const volumeId =
viewport
.getAllVolumeIds()
.find((_volumeId: string) => _volumeId.includes(displaySetInstanceUID)) ??
viewport.getVolumeId();
viewport.setProperties({ colormap }, volumeId);
}
if (immediate) { if (immediate) {
viewport.render(); viewport.render();
@ -1415,9 +1419,7 @@ function commandsModule({
if (!viewport) { if (!viewport) {
return; return;
} }
viewport.setProperties({ ops.setPreset(viewport, preset);
preset,
});
viewport.render(); viewport.render();
}, },
@ -1429,20 +1431,10 @@ function commandsModule({
setVolumeRenderingQulaity: ({ viewportId, volumeQuality }) => { setVolumeRenderingQulaity: ({ viewportId, volumeQuality }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0]; if (!viewport) {
const mapper = actor.getMapper(); return;
const image = mapper.getInputData(); }
const dims = image.getDimensions(); ops.setVolumeRenderingQuality(viewport, volumeQuality);
const spacing = image.getSpacing();
const spatialDiagonal = vec3.length(
vec3.fromValues(dims[0] * spacing[0], dims[1] * spacing[1], dims[2] * spacing[2])
);
let sampleDistance = spacing.reduce((a, b) => a + b) / 3.0;
sampleDistance /= volumeQuality > 1 ? 0.5 * volumeQuality ** 2 : 1.0;
const samplesPerRay = spatialDiagonal / sampleDistance + 1;
mapper.setMaximumSamplesPerRay(samplesPerRay);
mapper.setSampleDistance(sampleDistance);
viewport.render(); viewport.render();
}, },
@ -1453,27 +1445,10 @@ function commandsModule({
*/ */
shiftVolumeOpacityPoints: ({ viewportId, shift }) => { shiftVolumeOpacityPoints: ({ viewportId, shift }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0]; if (!viewport) {
const ofun = actor.getProperty().getScalarOpacity(0); return;
const opacityPointValues = []; // Array to hold values
// Gather Existing Values
const size = ofun.getSize();
for (let pointIdx = 0; pointIdx < size; pointIdx++) {
const opacityPointValue = [0, 0, 0, 0];
ofun.getNodeValue(pointIdx, opacityPointValue);
// opacityPointValue now holds [xLocation, opacity, midpoint, sharpness]
opacityPointValues.push(opacityPointValue);
} }
// Add offset ops.shiftVolumeOpacityPoints(viewport, shift);
opacityPointValues.forEach(opacityPointValue => {
opacityPointValue[0] += shift; // Change the location value
});
// Set new values
ofun.removeAllPoints();
opacityPointValues.forEach(opacityPointValue => {
ofun.addPoint(...opacityPointValue);
});
viewport.render(); viewport.render();
}, },
@ -1489,25 +1464,10 @@ function commandsModule({
setVolumeLighting: ({ viewportId, options }) => { setVolumeLighting: ({ viewportId, options }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
const { actor } = viewport.getActors()[0]; if (!viewport) {
const property = actor.getProperty(); return;
if (options.shade !== undefined) {
property.setShade(options.shade);
} }
ops.setVolumeLighting(viewport, options);
if (options.ambient !== undefined) {
property.setAmbient(options.ambient);
}
if (options.diffuse !== undefined) {
property.setDiffuse(options.diffuse);
}
if (options.specular !== undefined) {
property.setSpecular(options.specular);
}
viewport.render(); viewport.render();
}, },
resetCrosshairs: ({ viewportId }) => { resetCrosshairs: ({ viewportId }) => {
@ -1515,7 +1475,13 @@ function commandsModule({
const getCrosshairInstances = toolGroupId => { const getCrosshairInstances = toolGroupId => {
const toolGroup = toolGroupService.getToolGroup(toolGroupId); const toolGroup = toolGroupService.getToolGroup(toolGroupId);
crosshairInstances.push(toolGroup.getToolInstance('Crosshairs')); // Only fetch the instance when Crosshairs is registered in this tool
// group. getToolInstance logs a warning for an unregistered tool, and a
// viewport's default tool group does not always include Crosshairs (e.g.
// next viewports), which made Reset Viewport log a spurious warning.
if (toolGroup?.hasTool('Crosshairs')) {
crosshairInstances.push(toolGroup.getToolInstance('Crosshairs'));
}
}; };
if (!viewportId) { if (!viewportId) {
@ -1523,7 +1489,9 @@ function commandsModule({
toolGroupIds.forEach(getCrosshairInstances); toolGroupIds.forEach(getCrosshairInstances);
} else { } else {
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId); const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
getCrosshairInstances(toolGroup.id); if (toolGroup) {
getCrosshairInstances(toolGroup.id);
}
} }
crosshairInstances.forEach(ins => { crosshairInstances.forEach(ins => {
@ -1735,12 +1703,14 @@ function commandsModule({
* Removes a segmentation from the viewport * Removes a segmentation from the viewport
* @param props.segmentationId - The ID of the segmentation to remove * @param props.segmentationId - The ID of the segmentation to remove
*/ */
removeSegmentationFromViewportCommand: ({ segmentationId }) => { removeSegmentationFromViewportCommand: ({ segmentationId: displaySetInstanceUID }) => {
const { segmentationService, viewportGridService } = servicesManager.services; const { viewportGridService } = servicesManager.services;
segmentationService.removeSegmentationRepresentations( const viewportId = viewportGridService.getActiveViewportId();
viewportGridService.getActiveViewportId(),
{ segmentationId } commandsManager.runCommand('removeDisplaySetLayer', {
); viewportId,
displaySetInstanceUID,
});
}, },
/** /**
@ -1907,8 +1877,15 @@ function commandsModule({
* Use it before initializing the toolGroup with the tools. * Use it before initializing the toolGroup with the tools.
*/ */
initializeSegmentLabelTool: ({ tools }) => { initializeSegmentLabelTool: ({ tools }) => {
const appConfig = extensionManager.appConfig; const { customizationService } = servicesManager.services;
const segmentLabelConfig = appConfig.segmentation?.segmentLabel; const segmentLabelConfig = customizationService.getCustomization(
'segmentation.segmentLabel'
) as {
enabledByDefault?: boolean;
labelColor?: number[];
hoverTimeout?: number;
background?: string;
};
if (segmentLabelConfig?.enabledByDefault) { if (segmentLabelConfig?.enabledByDefault) {
const activeTools = tools?.active ?? []; const activeTools = tools?.active ?? [];
@ -1968,6 +1945,24 @@ function commandsModule({
rejectPreview: () => { rejectPreview: () => {
actions._handlePreviewAction('reject'); actions._handlePreviewAction('reject');
}, },
/**
* Generic Escape handler. A single Escape press should discard whatever the
* user has in progress, but that can be one of two unrelated things: a
* provisional segmentation preview, or an annotation being drawn. Rather
* than bind both `rejectPreview` and `cancelMeasurement` to `esc` (Mousetrap
* keeps only one handler per key, so the second silently shadows the first),
* this command orchestrates both single-purpose commands. Each is a no-op
* when its state is not active, so running both is safe and order-independent.
*/
cancelActiveOperation: () => {
try {
actions.rejectPreview();
} catch (error) {
console.debug('Error rejecting active preview', error);
} finally {
actions.cancelMeasurement();
}
},
clearMarkersForMarkerLabelmap: () => { clearMarkersForMarkerLabelmap: () => {
const { viewport } = _getActiveViewportEnabledElement(); const { viewport } = _getActiveViewportEnabledElement();
const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroupForViewport(viewport.id); const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroupForViewport(viewport.id);
@ -2065,24 +2060,45 @@ function commandsModule({
} }
segmentationService.addSegment(activeSegmentation.segmentationId); segmentationService.addSegment(activeSegmentation.segmentationId);
}, },
loadSegmentationDisplaySetsForViewport: ({ viewportId, displaySetInstanceUIDs }) => { loadSegmentationDisplaySetsForViewport: ({
viewportId,
displaySetInstanceUIDs,
viewportType,
}) => {
const updatedViewports = getUpdatedViewportsForSegmentation({ const updatedViewports = getUpdatedViewportsForSegmentation({
viewportId, viewportId,
servicesManager, servicesManager,
displaySetInstanceUIDs, displaySetInstanceUIDs,
}); });
if (!updatedViewports?.length) {
return;
}
updatedViewports.forEach(({ viewportId: csViewportId }) => {
const csViewport = cornerstoneViewportService.getCornerstoneViewport(csViewportId);
csViewport?.setNeedsRender?.();
});
actions.setDisplaySetsForViewports({ actions.setDisplaySetsForViewports({
viewportsToUpdate: updatedViewports.map(viewport => ({ viewportsToUpdate: updatedViewports.map(viewport => ({
viewportId: viewport.viewportId, viewportId: viewport.viewportId,
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs, displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
// When the caller pins a viewportType (RTSTRUCT contour hydration on a
// native "next" viewport requests 'stack'), force it so the referenced
// image stays in that render mode instead of resolving to a volume slice.
...(viewportType
? { viewportOptions: { ...viewport.viewportOptions, viewportType } }
: {}),
})), })),
}); });
}, },
setViewportOrientation: ({ viewportId, orientation }) => { setViewportOrientation: ({ viewportId, orientation }) => {
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
if (!viewport || viewport.type !== CoreEnums.ViewportType.ORTHOGRAPHIC) { // Accept any viewport already rendering volume content (legacy ORTHOGRAPHIC
// or a native viewport in volume mode) — both expose setOrientation().
if (!viewport || !getViewportAdapter(viewport).canReorientInPlace()) {
console.warn('Orientation can only be set on volume viewports'); console.warn('Orientation can only be set on volume viewports');
return; return;
} }
@ -2154,50 +2170,12 @@ function commandsModule({
viewportId?: string; viewportId?: string;
rotationMode?: 'apply' | 'set'; rotationMode?: 'apply' | 'set';
}) => { }) => {
const enabledElement = viewportId const viewport = _resolveViewport(viewportId);
? _getViewportEnabledElement(viewportId) if (!viewport) {
: _getActiveViewportEnabledElement();
if (!enabledElement) {
return; return;
} }
ops.rotate(viewport, rotation, rotationMode);
const { viewport } = enabledElement; viewport.render();
if (viewport instanceof BaseVolumeViewport) {
const camera = viewport.getCamera();
const rotAngle = (rotation * Math.PI) / 180;
const rotMat = mat4.identity(new Float32Array(16));
mat4.rotate(rotMat, rotMat, rotAngle, camera.viewPlaneNormal);
const rotatedViewUp = vec3.transformMat4(vec3.create(), camera.viewUp, rotMat);
viewport.setCamera({ viewUp: rotatedViewUp as CoreTypes.Point3 });
viewport.render();
return;
}
if (viewport.getRotation !== undefined) {
const { rotation: currentRotation } = viewport.getViewPresentation();
const newRotation =
rotationMode === 'apply'
? (currentRotation + rotation + 360) % 360
: (() => {
// In 'set' mode, account for the effect horizontal/vertical flips
// have on the perceived rotation direction. A single flip mirrors
// the image and inverses rotation direction, while two flips
// restore the original parity. We therefore invert the rotation
// angle when an odd number of flips are applied so that the
// requested absolute rotation matches the user expectation.
const { flipHorizontal = false, flipVertical = false } =
viewport.getViewPresentation();
const flipsParity = (flipHorizontal ? 1 : 0) + (flipVertical ? 1 : 0);
const effectiveRotation = flipsParity % 2 === 1 ? -rotation : rotation;
return (effectiveRotation + 360) % 360;
})();
viewport.setViewPresentation({ rotation: newRotation });
viewport.render();
}
}, },
startRecordingForAnnotationGroup: () => { startRecordingForAnnotationGroup: () => {
cornerstoneTools.AnnotationTool.startGroupRecording(); cornerstoneTools.AnnotationTool.startGroupRecording();
@ -2340,14 +2318,25 @@ function commandsModule({
targetSegmentInfo?: SegmentInfo; targetSegmentInfo?: SegmentInfo;
}) => { }) => {
if (!targetSegmentInfo) { if (!targetSegmentInfo) {
const sourceSegmentation = segmentationService.getSegmentation(
sourceSegmentInfo.segmentationId
);
const sourceCachedStats =
sourceSegmentation?.segments?.[sourceSegmentInfo.segmentIndex]?.cachedStats;
targetSegmentInfo = { targetSegmentInfo = {
segmentationId: sourceSegmentInfo.segmentationId, segmentationId: sourceSegmentInfo.segmentationId,
segmentIndex: segmentationService.getNextAvailableSegmentIndex( segmentIndex: segmentationService.getNextAvailableSegmentIndex(
sourceSegmentInfo.segmentationId sourceSegmentInfo.segmentationId
), ),
}; };
// Copy source cachedStats so jump-to-segment navigation works on the duplicate segment.
segmentationService.addSegment(targetSegmentInfo.segmentationId, { segmentationService.addSegment(targetSegmentInfo.segmentationId, {
segmentIndex: targetSegmentInfo.segmentIndex, segmentIndex: targetSegmentInfo.segmentIndex,
...(sourceCachedStats && {
cachedStats: csUtils.deepClone(sourceCachedStats) as Record<string, unknown>,
}),
}); });
} }
@ -2470,6 +2459,9 @@ function commandsModule({
removeMeasurement: { removeMeasurement: {
commandFn: actions.removeMeasurement, commandFn: actions.removeMeasurement,
}, },
cancelMeasurement: {
commandFn: actions.cancelMeasurement,
},
toggleLockMeasurement: { toggleLockMeasurement: {
commandFn: actions.toggleLockMeasurement, commandFn: actions.toggleLockMeasurement,
}, },
@ -2718,6 +2710,7 @@ function commandsModule({
toggleSegmentSelect: actions.toggleSegmentSelect, toggleSegmentSelect: actions.toggleSegmentSelect,
acceptPreview: actions.acceptPreview, acceptPreview: actions.acceptPreview,
rejectPreview: actions.rejectPreview, rejectPreview: actions.rejectPreview,
cancelActiveOperation: actions.cancelActiveOperation,
toggleUseCenterSegmentIndex: actions.toggleUseCenterSegmentIndex, toggleUseCenterSegmentIndex: actions.toggleUseCenterSegmentIndex,
toggleLabelmapAssist: actions.toggleLabelmapAssist, toggleLabelmapAssist: actions.toggleLabelmapAssist,
interpolateScrollForMarkerLabelmap: actions.interpolateScrollForMarkerLabelmap, interpolateScrollForMarkerLabelmap: actions.interpolateScrollForMarkerLabelmap,

View File

@ -60,11 +60,11 @@ const DicomUploadProgressItem = memo(
/> />
); );
case UploadStatus.InProgress: case UploadStatus.InProgress:
return <Icons.ByName name="icon-transferring" />; return <Icons.ByName name="icon-transferring" className="text-highlight" />;
case UploadStatus.Failed: case UploadStatus.Failed:
return <Icons.ByName name="icon-alert-small" />; return <Icons.ByName name="icon-alert-small" className="text-destructive" />;
case UploadStatus.Cancelled: case UploadStatus.Cancelled:
return <Icons.ByName name="icon-alert-outline" />; return <Icons.ByName name="icon-alert-outline" className="text-highlight" />;
default: default:
return <></>; return <></>;
} }

View File

@ -5,9 +5,7 @@ import {
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuPortal, DropdownMenuPortal,
DropdownMenuSubContent, DropdownMenuSubContent,
DropdownMenuLabel,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSeparator,
Icons, Icons,
} from '@ohif/ui-next'; } from '@ohif/ui-next';
@ -19,8 +17,6 @@ interface ExportSegmentationSubMenuItemProps {
allowExport: boolean; allowExport: boolean;
actions: { actions: {
storeSegmentation: (segmentationId: string, modality?: string) => Promise<unknown>; storeSegmentation: (segmentationId: string, modality?: string) => Promise<unknown>;
onSegmentationDownloadRTSS: (segmentationId: string) => void;
onSegmentationDownload: (segmentationId: string) => void;
downloadCSVSegmentationReport: (segmentationId: string) => void; downloadCSVSegmentationReport: (segmentationId: string) => void;
}; };
} }
@ -37,14 +33,10 @@ export const ExportSegmentationSubMenuItem: React.FC<ExportSegmentationSubMenuIt
<DropdownMenuSub> <DropdownMenuSub>
<DropdownMenuSubTrigger className="pl-1"> <DropdownMenuSubTrigger className="pl-1">
<Icons.Export className="text-foreground" /> <Icons.Export className="text-foreground" />
<span className="pl-2">{t('Download & Export')}</span> <span className="pl-2">{t('Export')}</span>
</DropdownMenuSubTrigger> </DropdownMenuSubTrigger>
<DropdownMenuPortal> <DropdownMenuPortal>
<DropdownMenuSubContent> <DropdownMenuSubContent>
<DropdownMenuLabel className="flex items-center pl-0">
<Icons.Download className="h-5 w-5" />
<span className="pl-1">{t('Download')}</span>
</DropdownMenuLabel>
{segmentationRepresentationType === SegmentationRepresentations.Labelmap && ( {segmentationRepresentationType === SegmentationRepresentations.Labelmap && (
<DropdownMenuItem <DropdownMenuItem
onClick={e => { onClick={e => {
@ -56,31 +48,6 @@ export const ExportSegmentationSubMenuItem: React.FC<ExportSegmentationSubMenuIt
{t('CSV Report')} {t('CSV Report')}
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{segmentationRepresentationType === SegmentationRepresentations.Labelmap && (
<DropdownMenuItem
onClick={e => {
e.preventDefault();
actions.onSegmentationDownload(segmentationId);
}}
disabled={!allowExport}
>
{t('DICOM SEG')}
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={e => {
e.preventDefault();
actions.onSegmentationDownloadRTSS(segmentationId);
}}
disabled={!allowExport}
>
{t('DICOM RTSS')}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuLabel className="flex items-center pl-0">
<Icons.Export className="h-5 w-5" />
<span className="pl-1 pt-1">{t('Export')}</span>
</DropdownMenuLabel>
{segmentationRepresentationType === SegmentationRepresentations.Labelmap && ( {segmentationRepresentationType === SegmentationRepresentations.Labelmap && (
<DropdownMenuItem <DropdownMenuItem
onClick={e => { onClick={e => {

View File

@ -18,7 +18,7 @@ function TrackingStatus({ viewportId }: { viewportId: string }) {
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<span> <span>
<Icons.StatusTracking className="h-4 w-4" /> <Icons.StatusTracking className="h-4 w-4 text-highlight" />
</span> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom"> <TooltipContent side="bottom">

View File

@ -83,10 +83,16 @@ const ViewportColorbarsContainer = memo(function ViewportColorbarsContainer({
const { displaySetInstanceUID: dsUID } = const { displaySetInstanceUID: dsUID } =
displaySetService.getDisplaySetByUID(displaySetInstanceUID) ?? {}; displaySetService.getDisplaySetByUID(displaySetInstanceUID) ?? {};
// Default the fused (horizontal) colorbar to the foreground (e.g. the PT
// in a PET/CT fusion), which is the meaningful layer. Only fall back to
// the background (CT) colorbar when the foreground has been explicitly
// faded to zero opacity. Previously a null/undefined opacity (e.g. before
// the hook resolved it) also fell through to the background, so the
// colorbar would flicker between CT and PT depending on timing.
const targetUID = const targetUID =
opacity === 0 || opacity == null opacity === 0
? backgroundDisplaySet?.displaySetInstanceUID ? backgroundDisplaySet?.displaySetInstanceUID
: foregroundDisplaySets[0].displaySetInstanceUID; : foregroundDisplaySets[0]?.displaySetInstanceUID;
return dsUID === targetUID; return dsUID === targetUID;
}); });

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { import {
Button, Button,
Icons, Icons,
@ -43,6 +43,10 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
const [thresholdOpacityEnabled, setThresholdOpacityEnabled] = useState(false); const [thresholdOpacityEnabled, setThresholdOpacityEnabled] = useState(false);
useEffect(() => {
setOptimisticOverlayDisplaySets(overlayDisplaySets);
}, [backgroundDisplaySet?.displaySetInstanceUID, overlayDisplaySets]);
/** /**
* Change the background display set * Change the background display set
*/ */
@ -482,7 +486,10 @@ function ViewportDataOverlayMenu({ viewportId }: withAppTypes<{ viewportId: stri
} }
}} }}
> >
<SelectTrigger className="flex-1"> <SelectTrigger
className="flex-1"
data-cy={`overlay-background-ds-select-${viewportId}`}
>
<SelectValue> <SelectValue>
{( {(
backgroundDisplaySet?.SeriesDescription || backgroundDisplaySet?.SeriesDescription ||

View File

@ -2,6 +2,7 @@ import React from 'react';
import { cn, Icons, useIconPresentation } from '@ohif/ui-next'; import { cn, Icons, useIconPresentation } from '@ohif/ui-next';
import { useSystem } from '@ohif/core'; import { useSystem } from '@ohif/core';
import { Enums } from '@cornerstonejs/core'; import { Enums } from '@cornerstonejs/core';
import { getViewportAdapter } from '../../services/ViewportService/adapter';
import { Popover, PopoverTrigger, PopoverContent, Button, useViewportGrid } from '@ohif/ui-next'; import { Popover, PopoverTrigger, PopoverContent, Button, useViewportGrid } from '@ohif/ui-next';
function ViewportOrientationMenu({ function ViewportOrientationMenu({
@ -36,8 +37,6 @@ function ViewportOrientationMenu({
const handleOrientationChange = (orientation: string) => { const handleOrientationChange = (orientation: string) => {
setCurrentOrientation(orientation); setCurrentOrientation(orientation);
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportIdToUse);
const currentViewportType = viewportInfo?.getViewportType();
if (!displaySets.length) { if (!displaySets.length) {
return; return;
@ -72,8 +71,17 @@ function ViewportOrientationMenu({
const displaySetUIDs = displaySets.map(ds => ds.displaySetInstanceUID); const displaySetUIDs = displaySets.map(ds => ds.displaySetInstanceUID);
// If viewport is not already a volume type, we need to convert it // A viewport already rendering in volume mode (legacy ORTHOGRAPHIC OR a next
if (currentViewportType !== Enums.ViewportType.ORTHOGRAPHIC) { // viewport that reports planarNext but renders volume actors, e.g. a CT+PET
// fusion) can be reoriented in place. Recreating it via setDisplaySetsForViewports
// would pass empty displaySetOptions and drop per-display-set presentation such
// as the PET overlay colormap/opacity. Only the genuine stack -> volume (MPR)
// case needs recreation.
const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportIdToUse);
const isVolumeMode = !!csViewport && getViewportAdapter(csViewport).canReorientInPlace();
// If viewport is not already in volume mode, we need to convert it
if (!isVolumeMode) {
// Configure the viewport to be a volume viewport with current display sets // Configure the viewport to be a volume viewport with current display sets
const updatedViewport = { const updatedViewport = {
viewportId: viewportIdToUse, viewportId: viewportIdToUse,

View File

@ -2,7 +2,7 @@ import React, { useEffect, useCallback, useState, ReactElement, useMemo } from '
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import debounce from 'lodash.debounce'; import debounce from 'lodash.debounce';
import { PanelSection, WindowLevel } from '@ohif/ui-next'; import { PanelSection, WindowLevel } from '@ohif/ui-next';
import { BaseVolumeViewport, Enums, eventTarget } from '@cornerstonejs/core'; import { Enums, eventTarget, utilities as csUtils, Types } from '@cornerstonejs/core';
import { useActiveViewportDisplaySets } from '@ohif/core'; import { useActiveViewportDisplaySets } from '@ohif/core';
import { import {
getNodeOpacity, getNodeOpacity,
@ -27,10 +27,16 @@ const ViewportWindowLevel = ({
const getViewportsWithVolumeIds = useCallback( const getViewportsWithVolumeIds = useCallback(
(volumeIds: string[]) => { (volumeIds: string[]) => {
const renderingEngine = cornerstoneViewportService.getRenderingEngine(); const renderingEngine = cornerstoneViewportService.getRenderingEngine();
const viewports = renderingEngine.getVolumeViewports(); // getVolumeViewports() was removed in the GenericViewport architecture
// (a PLANAR_NEXT viewport can be volume-capable without being a VolumeViewport).
// Official replacement: getViewports() + the viewportSupportsVolumeCompatibility
// capability guard (cornerstone codemod cornerstone3d/5/generic-viewport).
const viewports = renderingEngine
.getViewports()
.filter(csUtils.viewportSupportsVolumeCompatibility);
return viewports.filter(vp => { return viewports.filter(vp => {
const viewportVolumeIds = vp instanceof BaseVolumeViewport ? vp.getAllVolumeIds() : []; const viewportVolumeIds = (vp as Types.IVolumeViewport).getAllVolumeIds();
return ( return (
volumeIds.length === viewportVolumeIds.length && volumeIds.length === viewportVolumeIds.length &&
volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId)) volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId))
@ -124,8 +130,9 @@ const ViewportWindowLevel = ({
return; return;
} }
const viewportVolumeIds = const viewportVolumeIds = csUtils.viewportSupportsVolumeId(viewport)
viewport instanceof BaseVolumeViewport ? viewport.getAllVolumeIds() : []; ? (viewport as Types.IVolumeViewport).getAllVolumeIds()
: [];
const viewports = getViewportsWithVolumeIds(viewportVolumeIds); const viewports = getViewportsWithVolumeIds(viewportVolumeIds);
viewports.forEach(vp => { viewports.forEach(vp => {

View File

@ -2,6 +2,7 @@ import { cache as cs3DCache, Types } from '@cornerstonejs/core';
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps'; import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps';
import { utilities as csUtils } from '@cornerstonejs/core'; import { utilities as csUtils } from '@cornerstonejs/core';
import { getViewportVolumeHistogram } from './getViewportVolumeHistogram'; import { getViewportVolumeHistogram } from './getViewportVolumeHistogram';
import { getViewportAdapter } from '../../services/ViewportService/adapter';
/** /**
* Gets node opacity from volume actor * Gets node opacity from volume actor
@ -101,8 +102,18 @@ export const getWindowLevelsData = async (
return []; return [];
} }
const volumeIds = (viewport as Types.IBaseVolumeViewport).getAllVolumeIds(); // The per-volume histogram WL panel is a legacy volume-viewport feature; the
const viewportProperties = viewport.getProperties(); // adapter reports no volumeIds for native viewports (and legacy stacks), so
// those degrade gracefully to no histogram rows rather than erroring; the
// native WL path is driven by setViewportWindowLevel.
// TODO(next): port per-volume histograms to the native volume API.
const adapter = getViewportAdapter(viewport);
const volumeIds = adapter.getVolumeIds();
if (!volumeIds.length) {
return [];
}
const viewportProperties = adapter.getPresentation();
const { voiRange } = viewportProperties || {}; const { voiRange } = viewportProperties || {};
const viewportVoi = voiRange const viewportVoi = voiRange
? { ? {

View File

@ -1,15 +1,54 @@
import React, { ReactElement, useEffect, useRef, useState } from 'react'; import React, { ReactElement, useEffect, useRef, useState } from 'react';
import { AllInOneMenu, ScrollArea, Switch, Tabs, TabsList, TabsTrigger } from '@ohif/ui-next'; import { AllInOneMenu, ScrollArea, Switch, Tabs, TabsList, TabsTrigger } from '@ohif/ui-next';
import { useViewportRendering } from '../../hooks/useViewportRendering'; import { useViewportRendering } from '../../hooks/useViewportRendering';
import { useViewportDisplaySets } from '../../hooks/useViewportDisplaySets';
import { WindowLevelPreset } from '../../types/WindowLevel'; import { WindowLevelPreset } from '../../types/WindowLevel';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export function WindowLevel({ viewportId }: { viewportId?: string } = {}): ReactElement { export function WindowLevel({ viewportId }: { viewportId?: string } = {}): ReactElement {
const { t } = useTranslation('WindowLevelActionMenu'); const { t } = useTranslation('WindowLevelActionMenu');
const { viewportDisplaySets } = useViewportRendering(viewportId); const { viewportDisplaySets, foregroundDisplaySets } = useViewportDisplaySets(viewportId);
// Default the active tab to the foreground layer (e.g. the PT in a PET/CT
// fusion), matching the other window-level controls, instead of the grayscale
// background (CT) at index 0. The CT/PT tabs still let the user switch.
const defaultDisplaySetUID =
foregroundDisplaySets?.length > 0
? foregroundDisplaySets[foregroundDisplaySets.length - 1].displaySetInstanceUID
: viewportDisplaySets?.[0]?.displaySetInstanceUID;
const [activeDisplaySetUID, setActiveDisplaySetUID] = useState<string | undefined>( const [activeDisplaySetUID, setActiveDisplaySetUID] = useState<string | undefined>(
viewportDisplaySets?.[0]?.displaySetInstanceUID defaultDisplaySetUID
); );
// Tracks whether the user has explicitly picked a tab, so the foreground-default
// sync below stops overriding their choice.
const userSelectedRef = useRef(false);
// Adopt the foreground default if the display sets resolve after first render
// (and the user has not picked a tab yet). This must re-sync even when the
// initial render already seeded `activeDisplaySetUID` with the CT fallback
// (because `foregroundDisplaySets` was still empty at mount) — otherwise the
// tab stays pinned to CT once the foreground (PT) layer resolves.
useEffect(() => {
// If the active tab's display set is no longer in the viewport (e.g. the
// viewport switched to a different study/series), drop the now-stale
// selection — including a user pick — so it re-defaults instead of leaving an
// invalid UID that later fails validateActiveDisplaySet.
const activeStillPresent = viewportDisplaySets?.some(
ds => ds.displaySetInstanceUID === activeDisplaySetUID
);
if (activeDisplaySetUID && viewportDisplaySets?.length && !activeStillPresent) {
userSelectedRef.current = false;
setActiveDisplaySetUID(defaultDisplaySetUID);
return;
}
if (
!userSelectedRef.current &&
defaultDisplaySetUID &&
activeDisplaySetUID !== defaultDisplaySetUID
) {
setActiveDisplaySetUID(defaultDisplaySetUID);
}
}, [activeDisplaySetUID, defaultDisplaySetUID, viewportDisplaySets]);
// Use the hook with the active display set // Use the hook with the active display set
const { windowLevelPresets, setWindowLevel } = useViewportRendering(viewportId, { const { windowLevelPresets, setWindowLevel } = useViewportRendering(viewportId, {
@ -49,6 +88,7 @@ export function WindowLevel({ viewportId }: { viewportId?: string } = {}): React
<Tabs <Tabs
value={activeDisplaySetUID} value={activeDisplaySetUID}
onValueChange={displaySetUID => { onValueChange={displaySetUID => {
userSelectedRef.current = true;
setActiveDisplaySetUID(displaySetUID); setActiveDisplaySetUID(displaySetUID);
}} }}
> >

View File

@ -64,12 +64,6 @@ export const CustomDropdownMenuContent = () => {
context: 'CORNERSTONE', context: 'CORNERSTONE',
}); });
}, },
onSegmentationDownloadRTSS: segmentationId => {
commandsManager.run('downloadRTSS', { segmentationId });
},
onSegmentationDownload: segmentationId => {
commandsManager.run('downloadSegmentation', { segmentationId });
},
downloadCSVSegmentationReport: segmentationId => { downloadCSVSegmentationReport: segmentationId => {
commandsManager.run('downloadCSVSegmentationReport', { segmentationId }); commandsManager.run('downloadCSVSegmentationReport', { segmentationId });
}, },

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { ImageModal, FooterAction } from '@ohif/ui-next'; import { ImageModal, FooterAction, toast } from '@ohif/ui-next';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const MAX_TEXTURE_SIZE = 10000; const MAX_TEXTURE_SIZE = 10000;
const DEFAULT_FILENAME = 'image'; const DEFAULT_FILENAME = 'image';
@ -16,6 +16,7 @@ interface ViewportDownloadFormNewProps {
onEnableViewport: (element: HTMLElement) => void; onEnableViewport: (element: HTMLElement) => void;
onDisableViewport: () => void; onDisableViewport: () => void;
onDownload: (filename: string, fileType: string) => void; onDownload: (filename: string, fileType: string) => void;
onCopyToClipboard: () => void;
warningState: { enabled: boolean; value: string }; warningState: { enabled: boolean; value: string };
} }
@ -32,6 +33,7 @@ function ViewportDownloadFormNew({
onEnableViewport, onEnableViewport,
onDisableViewport, onDisableViewport,
onDownload, onDownload,
onCopyToClipboard,
}: ViewportDownloadFormNewProps) { }: ViewportDownloadFormNewProps) {
const [viewportElement, setViewportElement] = useState<HTMLElement | null>(null); const [viewportElement, setViewportElement] = useState<HTMLElement | null>(null);
const [showWarningMessage, setShowWarningMessage] = useState(true); const [showWarningMessage, setShowWarningMessage] = useState(true);
@ -138,13 +140,27 @@ function ViewportDownloadFormNew({
<FooterAction.Secondary onClick={onClose}> <FooterAction.Secondary onClick={onClose}>
{t('Common:Cancel')} {t('Common:Cancel')}
</FooterAction.Secondary> </FooterAction.Secondary>
<FooterAction.Secondary
onClick={async () => {
try {
await onCopyToClipboard();
toast.success(t('Image copied to clipboard'));
onClose();
} catch (error) {
toast.error(t('Failed to copy image to clipboard'));
console.error('Failed to copy to clipboard:', error);
}
}}
>
{t('Copy to Clipboard')}
</FooterAction.Secondary>
<FooterAction.Primary <FooterAction.Primary
onClick={() => { onClick={() => {
onDownload(filename || DEFAULT_FILENAME, fileType); onDownload(filename || DEFAULT_FILENAME, fileType);
onClose(); onClose();
}} }}
> >
{t('Common:Save')} {t('Save Image')}
</FooterAction.Primary> </FooterAction.Primary>
</FooterAction.Right> </FooterAction.Right>
</FooterAction> </FooterAction>

View File

@ -1,8 +1,11 @@
import type { Button } from '@ohif/core/types'; import type { Button } from '@ohif/core/types';
import { ViewportGridService } from '@ohif/core'; import { ViewportGridService, ToolbarService } from '@ohif/core';
import i18n from 'i18next'; import i18n from 'i18next';
import { MIN_SEGMENTATION_DRAWING_RADIUS, MAX_SEGMENTATION_DRAWING_RADIUS } from './constants'; const { TOOLBAR_SECTIONS } = ToolbarService;
export const MIN_SEGMENTATION_DRAWING_RADIUS = 0.5;
export const MAX_SEGMENTATION_DRAWING_RADIUS = 99.5;
const setToolActiveToolbar = { const setToolActiveToolbar = {
commandName: 'setToolActiveToolbar', commandName: 'setToolActiveToolbar',
@ -20,155 +23,16 @@ const callbacks = (toolName: string) => [
}, },
]; ];
export const toolbarButtons: Button[] = [ /**
{ * Segmentation editing toolbar buttons: the toolbox section containers plus
id: 'AdvancedRenderingControls', * the labelmap / contour editing tools and utilities. These complement the
uiType: 'ohif.advancedRenderingControls', * general buttons in `cornerstone.toolbarButtons`; together they are the
props: { * default button set for the segmentation mode, and modes such as basic /
buttonSection: true, * longitudinal can pull them in via a customization (see
}, * `segmentationEditing.jsonc`).
}, */
{ const segmentationToolbarButtons: Button[] = [
id: 'modalityLoadBadge', // section containers for the nested toolboxes and toolbars
uiType: 'ohif.modalityLoadBadge',
props: {
icon: 'Status',
label: i18n.t('Buttons:Status'),
tooltip: i18n.t('Buttons:Status'),
evaluate: {
name: 'evaluate.modalityLoadBadge',
hideWhenDisabled: true,
},
},
},
{
id: 'navigationComponent',
uiType: 'ohif.navigationComponent',
props: {
icon: 'Navigation',
label: i18n.t('Buttons:Navigation'),
tooltip: i18n.t('Buttons:Navigate between segments/measurements and manage their visibility'),
evaluate: {
name: 'evaluate.navigationComponent',
hideWhenDisabled: true,
},
},
},
{
id: 'trackingStatus',
uiType: 'ohif.trackingStatus',
props: {
icon: 'TrackingStatus',
label: i18n.t('Buttons:Tracking Status'),
tooltip: i18n.t('Buttons:View and manage tracking status of measurements and annotations'),
evaluate: {
name: 'evaluate.trackingStatus',
hideWhenDisabled: true,
},
},
},
{
id: 'dataOverlayMenu',
uiType: 'ohif.dataOverlayMenu',
props: {
icon: 'ViewportViews',
label: i18n.t('Buttons:Data Overlay'),
tooltip: i18n.t(
'Buttons:Configure data overlay options and manage foreground/background display sets'
),
evaluate: 'evaluate.dataOverlayMenu',
},
},
{
id: 'orientationMenu',
uiType: 'ohif.orientationMenu',
props: {
icon: 'OrientationSwitch',
label: i18n.t('Buttons:Orientation'),
tooltip: i18n.t(
'Buttons:Change viewport orientation between axial, sagittal, coronal and reformat planes'
),
evaluate: {
name: 'evaluate.orientationMenu',
// hideWhenDisabled: true,
},
},
},
{
id: 'windowLevelMenuEmbedded',
uiType: 'ohif.windowLevelMenuEmbedded',
props: {
icon: 'WindowLevel',
label: i18n.t('Buttons:Window Level'),
tooltip: i18n.t('Buttons:Adjust window/level presets and customize image contrast settings'),
evaluate: {
name: 'evaluate.windowLevelMenuEmbedded',
hideWhenDisabled: true,
},
},
},
{
id: 'windowLevelMenu',
uiType: 'ohif.windowLevelMenu',
props: {
icon: 'WindowLevel',
label: i18n.t('Buttons:Window Level'),
tooltip: i18n.t('Buttons:Adjust window/level presets and customize image contrast settings'),
evaluate: 'evaluate.windowLevelMenu',
},
},
{
id: 'voiManualControlMenu',
uiType: 'ohif.voiManualControlMenu',
props: {
icon: 'WindowLevelAdvanced',
label: i18n.t('Buttons:Advanced Window Level'),
tooltip: i18n.t('Buttons:Advanced window/level settings with manual controls and presets'),
evaluate: 'evaluate.voiManualControlMenu',
},
},
{
id: 'thresholdMenu',
uiType: 'ohif.thresholdMenu',
props: {
icon: 'Threshold',
label: i18n.t('Buttons:Threshold'),
tooltip: i18n.t('Buttons:Image threshold settings'),
evaluate: {
name: 'evaluate.thresholdMenu',
hideWhenDisabled: true,
},
},
},
{
id: 'opacityMenu',
uiType: 'ohif.opacityMenu',
props: {
icon: 'Opacity',
label: i18n.t('Buttons:Opacity'),
tooltip: i18n.t('Buttons:Image opacity settings'),
evaluate: {
name: 'evaluate.opacityMenu',
hideWhenDisabled: true,
},
},
},
{
id: 'Colorbar',
uiType: 'ohif.colorbar',
props: {
type: 'tool',
label: i18n.t('Buttons:Colorbar'),
},
},
// sections
{
id: 'MoreTools',
uiType: 'ohif.toolButtonList',
props: {
buttonSection: true,
},
},
{ {
id: 'BrushTools', id: 'BrushTools',
uiType: 'ohif.toolBoxButtonGroup', uiType: 'ohif.toolBoxButtonGroup',
@ -176,7 +40,6 @@ export const toolbarButtons: Button[] = [
buttonSection: true, buttonSection: true,
}, },
}, },
// Section containers for the nested toolboxes and toolbars.
{ {
id: 'LabelMapUtilities', id: 'LabelMapUtilities',
uiType: 'ohif.Toolbar', uiType: 'ohif.Toolbar',
@ -206,215 +69,6 @@ export const toolbarButtons: Button[] = [
}, },
}, },
// tool defs // tool defs
{
id: 'Zoom',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-zoom',
label: i18n.t('Buttons:Zoom'),
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
{
id: 'WindowLevel',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-window-level',
label: i18n.t('Buttons:Window Level'),
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
{
id: 'Pan',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-move',
label: i18n.t('Buttons:Pan'),
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
{
id: 'TrackballRotate',
uiType: 'ohif.toolButton',
props: {
type: 'tool',
icon: 'tool-3d-rotate',
label: i18n.t('Buttons:3D Rotate'),
commands: setToolActiveToolbar,
evaluate: {
name: 'evaluate.cornerstoneTool',
disabledText: i18n.t('Buttons:Select a 3D viewport to enable this tool'),
},
},
},
{
id: 'Capture',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-capture',
label: i18n.t('Buttons:Capture'),
commands: 'showDownloadViewportModal',
evaluate: [
'evaluate.action',
{
name: 'evaluate.viewport.supported',
unsupportedViewportTypes: ['video', 'wholeSlide'],
},
],
},
},
{
id: 'Layout',
uiType: 'ohif.layoutSelector',
props: {
rows: 3,
columns: 4,
evaluate: 'evaluate.action',
commands: 'setViewportGridLayout',
},
},
{
id: 'Crosshairs',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-crosshair',
label: i18n.t('Buttons:Crosshairs'),
commands: {
commandName: 'setToolActiveToolbar',
commandOptions: {
toolGroupIds: ['mpr'],
},
},
evaluate: {
name: 'evaluate.cornerstoneTool',
disabledText: i18n.t('Buttons:Select an MPR viewport to enable this tool'),
},
},
},
{
id: 'Reset',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-reset',
label: i18n.t('Buttons:Reset View'),
tooltip: i18n.t('Buttons:Reset View'),
commands: 'resetViewport',
evaluate: 'evaluate.action',
},
},
{
id: 'rotate-right',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-rotate-right',
label: i18n.t('Buttons:Rotate Right'),
tooltip: i18n.t('Buttons:Rotate +90'),
commands: 'rotateViewportCW',
evaluate: 'evaluate.action',
},
},
{
id: 'flipHorizontal',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-flip-horizontal',
label: i18n.t('Buttons:Flip Horizontal'),
tooltip: i18n.t('Buttons:Flip Horizontally'),
commands: 'flipViewportHorizontal',
evaluate: [
'evaluate.viewportProperties.toggle',
{
name: 'evaluate.viewport.supported',
unsupportedViewportTypes: ['volume3d'],
},
],
},
},
{
id: 'ReferenceLines',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-referenceLines',
label: i18n.t('Buttons:Reference Lines'),
tooltip: i18n.t('Buttons:Show Reference Lines'),
commands: 'toggleEnabledDisabledToolbar',
evaluate: 'evaluate.cornerstoneTool.toggle',
},
},
{
id: 'ImageOverlayViewer',
uiType: 'ohif.toolButton',
props: {
icon: 'toggle-dicom-overlay',
label: i18n.t('Buttons:Image Overlay'),
tooltip: i18n.t('Buttons:Toggle Image Overlay'),
commands: 'toggleEnabledDisabledToolbar',
evaluate: 'evaluate.cornerstoneTool.toggle',
},
},
{
id: 'StackScroll',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-stack-scroll',
label: i18n.t('Buttons:Stack Scroll'),
tooltip: i18n.t('Buttons:Stack Scroll'),
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
{
id: 'invert',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-invert',
label: i18n.t('Buttons:Invert'),
tooltip: i18n.t('Buttons:Invert Colors'),
commands: 'invertViewport',
evaluate: 'evaluate.viewportProperties.toggle',
},
},
{
id: 'Cine',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-cine',
label: i18n.t('Buttons:Cine'),
tooltip: i18n.t('Buttons:Cine'),
commands: 'toggleCine',
evaluate: [
'evaluate.cine',
{
name: 'evaluate.viewport.supported',
unsupportedViewportTypes: ['volume3d'],
},
],
},
},
{
id: 'Magnify',
uiType: 'ohif.toolButton',
props: {
icon: 'tool-magnify',
label: i18n.t('Buttons:Zoom-in'),
tooltip: i18n.t('Buttons:Zoom-in'),
commands: setToolActiveToolbar,
evaluate: 'evaluate.cornerstoneTool',
},
},
{
id: 'TagBrowser',
uiType: 'ohif.toolButton',
props: {
icon: 'dicom-tag-browser',
label: i18n.t('Buttons:Dicom Tag Browser'),
tooltip: i18n.t('Buttons:Dicom Tag Browser'),
commands: 'openDICOMTagViewer',
},
},
{ {
id: 'PlanarFreehandContourSegmentationTool', id: 'PlanarFreehandContourSegmentationTool',
uiType: 'ohif.toolBoxButton', uiType: 'ohif.toolBoxButton',
@ -559,7 +213,11 @@ export const toolbarButtons: Button[] = [
value: 'CatmullRomSplineROI', value: 'CatmullRomSplineROI',
label: i18n.t('Buttons:Catmull Rom Spline'), label: i18n.t('Buttons:Catmull Rom Spline'),
}, },
{ id: 'LinearSplineROI', value: 'LinearSplineROI', label: i18n.t('Buttons:Linear Spline') }, {
id: 'LinearSplineROI',
value: 'LinearSplineROI',
label: i18n.t('Buttons:Linear Spline'),
},
{ id: 'BSplineROI', value: 'BSplineROI', label: i18n.t('Buttons:B-Spline') }, { id: 'BSplineROI', value: 'BSplineROI', label: i18n.t('Buttons:B-Spline') },
], ],
commands: { commands: {
@ -763,18 +421,23 @@ export const toolbarButtons: Button[] = [
}, },
}, },
{ {
id: 'RegionSegmentPlus', id: 'ClickSegment',
uiType: 'ohif.toolBoxButton', uiType: 'ohif.toolBoxButton',
props: { props: {
icon: 'icon-tool-click-segment', icon: 'icon-tool-click-segment',
label: i18n.t('Buttons:One Click Segment'), label: i18n.t('Buttons:Click to Segment'),
tooltip: i18n.t( tooltip: i18n.t(
'Buttons:Detects segmentable regions with one click. Hover for visual feedback—click when a plus sign appears to auto-segment the lesion.' 'Buttons:PET only. Click-to-segment lesions with no configuration. Hover for visual feedback—click when a plus sign appears to segment the lesion.'
), ),
evaluate: [ evaluate: [
{
name: 'evaluate.modality.supported',
supportedModalities: ['PT'],
disabledText: i18n.t('Buttons:Tool not available for this modality'),
},
{ {
name: 'evaluate.cornerstone.segmentation', name: 'evaluate.cornerstone.segmentation',
toolNames: ['RegionSegmentPlus'], toolNames: ['ClickSegment'],
disabledText: i18n.t('Buttons:Create new segmentation to enable this tool.'), disabledText: i18n.t('Buttons:Create new segmentation to enable this tool.'),
}, },
{ {
@ -1195,4 +858,106 @@ export const toolbarButtons: Button[] = [
}, },
]; ];
export default toolbarButtons; /**
* The toolbox / utilities section wiring for segmentation editing. These are
* the sections rendered by the `panelSegmentationWithTools*` panels, so any
* mode that shows those panels can merge this block into its toolbar sections.
*/
export const segmentationToolboxSections: Record<string, string[]> = {
[TOOLBAR_SECTIONS.labelMapSegmentationToolbox]: ['LabelMapTools'],
[TOOLBAR_SECTIONS.contourSegmentationToolbox]: ['ContourTools'],
[TOOLBAR_SECTIONS.labelMapSegmentationUtilities]: ['LabelMapUtilities'],
[TOOLBAR_SECTIONS.contourSegmentationUtilities]: ['ContourUtilities'],
LabelMapTools: [
'LabelmapSlicePropagation',
'BrushTools',
'MarkerLabelmap',
'ClickSegment',
'Shapes',
'LabelMapEditWithContour',
],
ContourTools: [
'PlanarFreehandContourSegmentationTool',
'SculptorTool',
'SplineContourSegmentationTool',
'LivewireContourSegmentationTool',
],
LabelMapUtilities: ['InterpolateLabelmap', 'SegmentBidirectional'],
ContourUtilities: ['LogicalContourOperations', 'SimplifyContours', 'SmoothContours'],
BrushTools: ['Brush', 'Eraser', 'Threshold'],
};
/**
* The segmentation mode's main toolbar layout (primary bar and viewport
* action corners). Kept separate from the toolbox wiring above so other modes
* can adopt segmentation editing without adopting this mode layout.
*/
export const segmentationModeToolbarSections: Record<string, string[]> = {
[TOOLBAR_SECTIONS.primary]: [
'WindowLevel',
'Pan',
'Zoom',
'TrackballRotate',
'Capture',
'Layout',
'Crosshairs',
'MoreTools',
],
[TOOLBAR_SECTIONS.viewportActionMenu.topLeft]: ['orientationMenu', 'dataOverlayMenu'],
[TOOLBAR_SECTIONS.viewportActionMenu.bottomMiddle]: ['AdvancedRenderingControls'],
AdvancedRenderingControls: [
'windowLevelMenuEmbedded',
'voiManualControlMenu',
'Colorbar',
'opacityMenu',
'thresholdMenu',
],
[TOOLBAR_SECTIONS.viewportActionMenu.topRight]: [
'modalityLoadBadge',
'trackingStatus',
'navigationComponent',
],
[TOOLBAR_SECTIONS.viewportActionMenu.bottomLeft]: ['windowLevelMenu'],
MoreTools: [
'Reset',
'rotate-right',
'flipHorizontal',
'ReferenceLines',
'ImageOverlayViewer',
'StackScroll',
'invert',
'Cine',
'Magnify',
'TagBrowser',
],
};
/**
* Segmentation capability packs registered (at default scope) by the
* cornerstone extension. These are pure "what can exist" packs and carry no
* mode identity:
* - `cornerstone.segmentationToolbarButtons` segmentation editing button definitions
* - `cornerstone.segmentationToolbarSections` toolbox/utilities section wiring
* - `cornerstone.segmentationModeToolbarSections` a reusable segmentation-mode toolbar layout
*
* Modes compose these by name in their own `toolbarButtons` /
* `toolbarSections` instance arrays; `?customization=` modules extend the
* result through the `mode` phase.
*/
const segmentationToolbarCustomization = {
'cornerstone.segmentationToolbarButtons': segmentationToolbarButtons,
'cornerstone.segmentationToolbarSections': segmentationToolboxSections,
'cornerstone.segmentationModeToolbarSections': segmentationModeToolbarSections,
};
export { segmentationToolbarButtons };
export default segmentationToolbarCustomization;

View File

@ -0,0 +1,204 @@
import { toolNames } from '../initCornerstoneTools';
import {
MIN_SEGMENTATION_DRAWING_RADIUS,
MAX_SEGMENTATION_DRAWING_RADIUS,
} from './segmentationToolbarCustomization';
/**
* Reusable tool group "capability blocks", registered as default
* customizations so modes and `?customization=` JSON modules can add them to
* a tool group by name via a mode's `toolGroupAdditions` customization, e.g.
*
* "mode": {
* "basic": {
* "toolGroupAdditions": {
* "default": { "$push": [{ "$reference": "cornerstone.segmentationTools" }] }
* }
* }
* }
*
* Each block is a `{ active/passive/enabled/disabled }` object suitable for
* `toolGroupService.addToolsToToolGroup`.
*/
function getToolGroupToolsCustomization({ commandsManager }) {
const brushInstances = [
{
toolName: 'CircularBrush',
parentTool: 'Brush',
configuration: {
activeStrategy: 'FILL_INSIDE_CIRCLE',
minRadius: MIN_SEGMENTATION_DRAWING_RADIUS,
maxRadius: MAX_SEGMENTATION_DRAWING_RADIUS,
},
},
{
toolName: 'CircularEraser',
parentTool: 'Brush',
configuration: {
activeStrategy: 'ERASE_INSIDE_CIRCLE',
minRadius: MIN_SEGMENTATION_DRAWING_RADIUS,
maxRadius: MAX_SEGMENTATION_DRAWING_RADIUS,
},
},
{
toolName: 'SphereBrush',
parentTool: 'Brush',
configuration: {
activeStrategy: 'FILL_INSIDE_SPHERE',
minRadius: MIN_SEGMENTATION_DRAWING_RADIUS,
maxRadius: MAX_SEGMENTATION_DRAWING_RADIUS,
},
},
{
toolName: 'SphereEraser',
parentTool: 'Brush',
configuration: {
activeStrategy: 'ERASE_INSIDE_SPHERE',
minRadius: MIN_SEGMENTATION_DRAWING_RADIUS,
maxRadius: MAX_SEGMENTATION_DRAWING_RADIUS,
},
},
{
toolName: 'ThresholdCircularBrush',
parentTool: 'Brush',
configuration: {
activeStrategy: 'THRESHOLD_INSIDE_CIRCLE',
minRadius: MIN_SEGMENTATION_DRAWING_RADIUS,
maxRadius: MAX_SEGMENTATION_DRAWING_RADIUS,
},
},
{
toolName: 'ThresholdSphereBrush',
parentTool: 'Brush',
configuration: {
activeStrategy: 'THRESHOLD_INSIDE_SPHERE',
minRadius: MIN_SEGMENTATION_DRAWING_RADIUS,
maxRadius: MAX_SEGMENTATION_DRAWING_RADIUS,
},
},
{
toolName: 'ThresholdCircularBrushDynamic',
parentTool: 'Brush',
configuration: {
activeStrategy: 'THRESHOLD_INSIDE_CIRCLE',
minRadius: MIN_SEGMENTATION_DRAWING_RADIUS,
maxRadius: MAX_SEGMENTATION_DRAWING_RADIUS,
threshold: {
isDynamic: true,
dynamicRadius: 3,
},
},
},
{
toolName: 'ThresholdSphereBrushDynamic',
parentTool: 'Brush',
configuration: {
activeStrategy: 'THRESHOLD_INSIDE_SPHERE',
minRadius: MIN_SEGMENTATION_DRAWING_RADIUS,
maxRadius: MAX_SEGMENTATION_DRAWING_RADIUS,
threshold: {
isDynamic: true,
dynamicRadius: 3,
},
},
},
];
const splineInstances = [
{
toolName: 'CatmullRomSplineROI',
parentTool: toolNames.SplineContourSegmentation,
configuration: {
spline: {
type: 'CATMULLROM',
enableTwoPointPreview: true,
},
},
},
{
toolName: 'LinearSplineROI',
parentTool: toolNames.SplineContourSegmentation,
configuration: {
spline: {
type: 'LINEAR',
enableTwoPointPreview: true,
},
},
},
{
toolName: 'BSplineROI',
parentTool: toolNames.SplineContourSegmentation,
configuration: {
spline: {
type: 'BSPLINE',
enableTwoPointPreview: true,
},
},
},
];
return {
/**
* The segmentation editing tools (labelmap brushes/scissors and contour
* segmentation tools), matching the buttons in
* `cornerstone.segmentationToolbarButtons`.
*/
'cornerstone.segmentationTools': {
passive: [
...brushInstances,
{ toolName: toolNames.LabelmapSlicePropagation },
{ toolName: toolNames.MarkerLabelmap },
{ toolName: toolNames.ClickSegment },
{ toolName: toolNames.LabelMapEditWithContourTool },
{ toolName: toolNames.SegmentSelect },
{ toolName: toolNames.CircleScissors },
{ toolName: toolNames.RectangleScissors },
{ toolName: toolNames.SphereScissors },
{ toolName: toolNames.LivewireContourSegmentation },
{ toolName: toolNames.SculptorTool },
...splineInstances,
],
},
/**
* The measurement/annotation tools, matching the `MeasurementTools`
* buttons in `cornerstone.toolbarButtons`. Useful for adding annotations
* to modes (such as segmentation) whose tool groups omit them.
*/
'cornerstone.annotationTools': {
passive: [
{ toolName: toolNames.Length },
{
toolName: toolNames.ArrowAnnotate,
configuration: {
getTextCallback: (callback, eventDetails) => {
commandsManager.runCommand('arrowTextCallback', {
callback,
eventDetails,
});
},
changeTextCallback: (data, eventDetails, callback) => {
commandsManager.runCommand('arrowTextCallback', {
callback,
data,
eventDetails,
});
},
},
},
{ toolName: toolNames.Bidirectional },
{ toolName: toolNames.Probe },
{ toolName: toolNames.DragProbe },
{ toolName: toolNames.EllipticalROI },
{ toolName: toolNames.CircleROI },
{ toolName: toolNames.RectangleROI },
{ toolName: toolNames.Angle },
{ toolName: toolNames.CobbAngle },
{ toolName: toolNames.SplineROI },
{ toolName: toolNames.LivewireContour },
],
},
};
}
export default getToolGroupToolsCustomization;

Some files were not shown because too many files have changed in this diff Show More