Compare commits
3 Commits
v3.13.0-be
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| bbe37ff388 | |||
|
|
f7612cdd0a | ||
|
|
125a174298 |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dicom-pmap",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "DICOM Parametric Map read workflow",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dicom-rt",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "DICOM RT read workflow",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dicom-seg",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "DICOM SEG read workflow",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dicom-sr",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "OHIF extension for an SR Cornerstone Viewport",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dynamic-volume",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "OHIF extension for 4D volumes data",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
} from '@cornerstonejs/core';
|
||||
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
|
||||
import { utilities as csMetadataUtilities } from '@cornerstonejs/metadata';
|
||||
import { Types } from '@ohif/core';
|
||||
import { Types, AnnotationPersistenceService } from '@ohif/core';
|
||||
import Enums from './enums';
|
||||
|
||||
import init from './init';
|
||||
@ -216,6 +216,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
servicesManager.registerService(CornerstoneCacheService.REGISTRATION);
|
||||
servicesManager.registerService(ColorbarService.REGISTRATION);
|
||||
servicesManager.registerService(ViewedDataService.REGISTRATION);
|
||||
servicesManager.registerService(AnnotationPersistenceService.REGISTRATION);
|
||||
|
||||
const { syncGroupService } = servicesManager.services;
|
||||
syncGroupService.registerCustomSynchronizer('frameview', createFrameViewSynchronizer);
|
||||
|
||||
@ -462,6 +462,12 @@ const connectMeasurementServiceToTools = ({
|
||||
}
|
||||
|
||||
const { referenceSeriesUID, referenceStudyUID, SOPInstanceUID, metadata } = measurement;
|
||||
const persistedAnnotation = data?.annotation || {};
|
||||
const persistedData = persistedAnnotation.data || {};
|
||||
const persistedHandles = persistedData.handles || {};
|
||||
const handlePoints = Array.isArray(persistedHandles.points)
|
||||
? persistedHandles.points.filter(point => Array.isArray(point) && point.length >= 2)
|
||||
: [];
|
||||
|
||||
const instance = DicomMetadataStore.getInstance(
|
||||
referenceStudyUID,
|
||||
@ -506,11 +512,11 @@ const connectMeasurementServiceToTools = ({
|
||||
* Don't remove this destructuring of data here.
|
||||
* This is used to pass annotation specific data forward e.g. contour
|
||||
*/
|
||||
...(data.annotation.data || {}),
|
||||
text: data.annotation.data.text,
|
||||
handles: { ...data.annotation.data.handles },
|
||||
cachedStats: { ...data.annotation.data.cachedStats },
|
||||
label: data.annotation.data.label,
|
||||
...(persistedData || {}),
|
||||
text: persistedData.text,
|
||||
handles: { ...persistedHandles, points: handlePoints },
|
||||
cachedStats: { ...(persistedData.cachedStats || {}) },
|
||||
label: persistedData.label,
|
||||
frameNumber,
|
||||
},
|
||||
};
|
||||
@ -534,7 +540,9 @@ const connectMeasurementServiceToTools = ({
|
||||
commandsManager.run('cancelMeasurement');
|
||||
|
||||
const removedAnnotation = annotation.state.getAnnotation(removedMeasurementId);
|
||||
removeAnnotation(removedMeasurementId);
|
||||
if (removedAnnotation) {
|
||||
removeAnnotation(removedMeasurementId);
|
||||
}
|
||||
// Ensure `removedAnnotation` is available before triggering the memo,
|
||||
// as it can be undefined during an undo operation
|
||||
if (removedAnnotation) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-default",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "Common/default features and functionality for basic image viewing",
|
||||
"author": "OHIF Core Team",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-microscopy",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "OHIF extension for DICOM microscopy",
|
||||
"author": "Bill Wallace, md-prog",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-pdf",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "OHIF extension for PDF display",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-video",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "OHIF extension for video display",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-measurement-tracking",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "Tracking features and functionality for basic image viewing",
|
||||
"author": "OHIF Core Team",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-test",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "OHIF extension used inside e2e testing",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-tmtv",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "OHIF extension for Total Metabolic Tumor Volume",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-ultrasound-pleura-bline",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "",
|
||||
"author": "Rodrigo Basilio",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/mode-basic-dev-mode",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "Basic OHIF Viewer Using Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/mode-test",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "Basic mode for testing",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/mode-basic",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "A basic mode used to build other modes on top of",
|
||||
"author": "OHIF Contributors",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/mode-longitudinal",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "Longitudinal Workflow",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/mode-microscopy",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "OHIF mode for DICOM microscopy",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/mode-preclinical-4d",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "4D Workflow",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/mode-segmentation",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "OHIF segmentation mode which enables labelmap segmentation read/edit/export",
|
||||
"author": "@ohif",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/mode-tmtv",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "Total Metabolic Tumor Volume Workflow",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/mode-ultrasound-pleura-bline",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "Allows users to annotate ultrasound images with pleura B-line annotations.",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@ -124,5 +124,5 @@
|
||||
"git add"
|
||||
]
|
||||
},
|
||||
"version": "3.13.0-beta.124"
|
||||
"version": "3.13.0-beta.125"
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/app",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"productVersion": "3.4.0",
|
||||
"description": "OHIF Viewer",
|
||||
"author": "OHIF Contributors",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/cli",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "A CLI to bootstrap new OHIF extension or mode",
|
||||
"type": "module",
|
||||
"main": "src/index.js",
|
||||
|
||||
@ -2,7 +2,7 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { execa } from 'execa';
|
||||
import { keywords } from './enums/index.js';
|
||||
import { validateYarn, addExtensionToConfig, addModeToConfig } from './utils/index.js';
|
||||
import { validatePnpm, addExtensionToConfig, addModeToConfig } from './utils/index.js';
|
||||
|
||||
async function linkPackage(packageDir, options, addToConfig, keyword) {
|
||||
const { viewerDirectory } = options;
|
||||
@ -22,19 +22,18 @@ async function linkPackage(packageDir, options, addToConfig, keyword) {
|
||||
|
||||
const version = packageJSON.version;
|
||||
|
||||
// make sure yarn is installed
|
||||
await validateYarn();
|
||||
// make sure pnpm is installed
|
||||
await validatePnpm();
|
||||
|
||||
// change directory to packageDir and execute yarn link
|
||||
process.chdir(packageDir);
|
||||
// resolve the package directory before changing the working directory
|
||||
const resolvedPackageDir = path.resolve(packageDir);
|
||||
|
||||
let results;
|
||||
results = await execa(`yarn`, ['link']);
|
||||
|
||||
// change directory to OHIF Platform root and execute yarn link
|
||||
// change directory to the OHIF Platform root and link the local package there.
|
||||
// Linking mutates the lockfile, so disable the workspace's frozen-lockfile
|
||||
// default for this call (pnpm link rejects --no-frozen-lockfile).
|
||||
process.chdir(`${viewerDirectory}/../..`);
|
||||
|
||||
results = await execa(`yarn`, ['link', packageName]);
|
||||
let results = await execa('pnpm', ['link', resolvedPackageDir, '--config.frozen-lockfile=false']);
|
||||
console.log(results.stdout);
|
||||
|
||||
// Add the node_modules of the linked package so that webpack
|
||||
@ -63,7 +62,7 @@ async function linkPackage(packageDir, options, addToConfig, keyword) {
|
||||
});
|
||||
|
||||
// run prettier on the webpack config
|
||||
results = await execa(`yarn`, ['prettier', '--write', webpackPwaPath]);
|
||||
results = await execa('pnpm', ['exec', 'prettier', '--write', webpackPwaPath]);
|
||||
}
|
||||
|
||||
function linkExtension(packageDir, options) {
|
||||
|
||||
@ -1,18 +1,20 @@
|
||||
import { execa } from 'execa';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { validateYarn, removeExtensionFromConfig, removeModeFromConfig } from './utils/index.js';
|
||||
import { validatePnpm, removeExtensionFromConfig, removeModeFromConfig } from './utils/index.js';
|
||||
|
||||
const linkPackage = async (packageName, options, removeFromConfig) => {
|
||||
const { viewerDirectory } = options;
|
||||
|
||||
// make sure yarn is installed
|
||||
await validateYarn();
|
||||
// make sure pnpm is installed
|
||||
await validatePnpm();
|
||||
|
||||
// change directory to OHIF Platform root and execute yarn link
|
||||
process.chdir(viewerDirectory);
|
||||
// change directory to the OHIF Platform root, where the package was linked.
|
||||
// Unlinking mutates the lockfile, so disable the workspace's frozen-lockfile
|
||||
// default for this call (pnpm unlink rejects --no-frozen-lockfile).
|
||||
process.chdir(`${viewerDirectory}/../..`);
|
||||
|
||||
const results = await execa(`yarn`, ['unlink', packageName]);
|
||||
const results = await execa('pnpm', ['unlink', packageName, '--config.frozen-lockfile=false']);
|
||||
console.log(results.stdout);
|
||||
|
||||
const webpackPwaPath = path.join(viewerDirectory, '.webpack', 'webpack.pwa.js');
|
||||
@ -23,7 +25,7 @@ const linkPackage = async (packageName, options, removeFromConfig) => {
|
||||
removeFromConfig(packageName);
|
||||
|
||||
// run prettier on the webpack config
|
||||
await execa(`yarn`, ['prettier', '--write', webpackPwaPath]);
|
||||
await execa('pnpm', ['exec', 'prettier', '--write', webpackPwaPath]);
|
||||
};
|
||||
|
||||
async function removePathFromWebpackConfig(webpackConfigPath, packageName) {
|
||||
|
||||
@ -19,7 +19,7 @@ import editPackageJson from './editPackageJson.js';
|
||||
import createLicense from './createLicense.js';
|
||||
import createReadme from './createReadme.js';
|
||||
import prettyPrint from './prettyPrint.js';
|
||||
import validateYarn from './validateYarn.js';
|
||||
import validatePnpm from './validatePnpm.js';
|
||||
|
||||
export {
|
||||
getYarnInfo,
|
||||
@ -43,5 +43,5 @@ export {
|
||||
createLicense,
|
||||
createReadme,
|
||||
prettyPrint,
|
||||
validateYarn,
|
||||
validatePnpm,
|
||||
};
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import chalk from 'chalk';
|
||||
import { execa } from 'execa';
|
||||
|
||||
export default async function validateYarn() {
|
||||
export default async function validatePnpm() {
|
||||
try {
|
||||
await execa('yarn', ['--version']);
|
||||
await execa('pnpm', ['--version']);
|
||||
} catch (err) {
|
||||
console.log(
|
||||
'%s Yarn is not installed, please install it before linking your extension',
|
||||
'%s pnpm is not installed, please install it before linking your extension',
|
||||
chalk.red.bold('ERROR')
|
||||
);
|
||||
process.exit(1);
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/core",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "Generic business logic for web-based medical imaging applications",
|
||||
"author": "OHIF Core Team",
|
||||
"license": "MIT",
|
||||
|
||||
@ -35,6 +35,7 @@ import {
|
||||
WorkflowStepsService,
|
||||
StudyPrefetcherService,
|
||||
MultiMonitorService,
|
||||
AnnotationPersistenceService,
|
||||
} from './services';
|
||||
|
||||
import { DisplaySetMessage, DisplaySetMessageList } from './services/DisplaySetService';
|
||||
@ -91,6 +92,7 @@ const OHIF = {
|
||||
useActiveViewportDisplaySets,
|
||||
WorkflowStepsService,
|
||||
StudyPrefetcherService,
|
||||
AnnotationPersistenceService,
|
||||
};
|
||||
|
||||
export {
|
||||
@ -138,6 +140,7 @@ export {
|
||||
PanelService,
|
||||
WorkflowStepsService,
|
||||
StudyPrefetcherService,
|
||||
AnnotationPersistenceService,
|
||||
useSystem,
|
||||
useActiveViewportDisplaySets,
|
||||
};
|
||||
|
||||
@ -0,0 +1,259 @@
|
||||
import AnnotationPersistenceService from './AnnotationPersistenceService';
|
||||
|
||||
const mockAnnotationManager = { addAnnotation: jest.fn() };
|
||||
const mockAnnotations = new Map<string, any>();
|
||||
|
||||
jest.mock('@cornerstonejs/tools', () => ({
|
||||
annotation: {
|
||||
state: {
|
||||
getAnnotationManager: jest.fn(() => mockAnnotationManager),
|
||||
getAnnotation: jest.fn((uid: string) => mockAnnotations.get(uid)),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
class FakeMeasurementService {
|
||||
public EVENTS = {
|
||||
MEASUREMENT_ADDED: 'MEASUREMENT_ADDED',
|
||||
MEASUREMENT_UPDATED: 'MEASUREMENT_UPDATED',
|
||||
MEASUREMENT_REMOVED: 'MEASUREMENT_REMOVED',
|
||||
};
|
||||
|
||||
private listeners = new Map<string, Array<(event: any) => void>>();
|
||||
public measurements = new Map<string, any>();
|
||||
public addOrUpdateMeasurement = jest.fn((measurement: any) => {
|
||||
this.measurements.set(measurement.uid, measurement);
|
||||
return measurement.uid;
|
||||
});
|
||||
|
||||
public getSource = jest.fn((name: string, version: string) => {
|
||||
return { name, version, uid: 'test-source-uid' };
|
||||
});
|
||||
|
||||
public subscribe(eventName: string, callback: (event: any) => void) {
|
||||
if (!this.listeners.has(eventName)) {
|
||||
this.listeners.set(eventName, []);
|
||||
}
|
||||
this.listeners.get(eventName).push(callback);
|
||||
return { unsubscribe: () => undefined };
|
||||
}
|
||||
|
||||
public emit(eventName: string, payload: any) {
|
||||
this.listeners.get(eventName)?.forEach(callback => callback(payload));
|
||||
}
|
||||
}
|
||||
|
||||
class FakeDisplaySetService {
|
||||
public EVENTS = {
|
||||
DISPLAY_SETS_ADDED: 'DISPLAY_SETS_ADDED',
|
||||
};
|
||||
|
||||
private listeners = new Map<string, Array<(event: any) => void>>();
|
||||
|
||||
public subscribe(eventName: string, callback: (event: any) => void) {
|
||||
if (!this.listeners.has(eventName)) {
|
||||
this.listeners.set(eventName, []);
|
||||
}
|
||||
this.listeners.get(eventName).push(callback);
|
||||
return { unsubscribe: () => undefined };
|
||||
}
|
||||
|
||||
public emit(eventName: string, payload: any) {
|
||||
this.listeners.get(eventName)?.forEach(callback => callback(payload));
|
||||
}
|
||||
}
|
||||
|
||||
class FakeBackend {
|
||||
public records: any[] = [];
|
||||
|
||||
public save(record: any) {
|
||||
this.records.push(record);
|
||||
}
|
||||
|
||||
public update(record: any) {
|
||||
this.records = this.records.filter(existing => existing.uid !== record.uid);
|
||||
this.records.push(record);
|
||||
}
|
||||
|
||||
public delete(uid: string) {
|
||||
this.records = this.records.filter(existing => existing.uid !== uid);
|
||||
}
|
||||
|
||||
public load(studyInstanceUID: string) {
|
||||
return this.records.filter(entry => entry.studyInstanceUID === studyInstanceUID);
|
||||
}
|
||||
}
|
||||
|
||||
describe('AnnotationPersistenceService', () => {
|
||||
beforeEach(() => {
|
||||
mockAnnotationManager.addAnnotation.mockClear();
|
||||
mockAnnotations.clear();
|
||||
});
|
||||
|
||||
it('persists created measurements and restores them for a study', async () => {
|
||||
const backend = new FakeBackend();
|
||||
const measurementService = new FakeMeasurementService();
|
||||
const displaySetService = new FakeDisplaySetService();
|
||||
const service = new AnnotationPersistenceService(
|
||||
backend as any,
|
||||
{
|
||||
services: {
|
||||
measurementService: measurementService as any,
|
||||
displaySetService: displaySetService as any,
|
||||
},
|
||||
} as any
|
||||
);
|
||||
|
||||
const measurement = {
|
||||
uid: 'measurement-1',
|
||||
toolName: 'Length',
|
||||
label: 'Length',
|
||||
referenceStudyUID: 'study-1',
|
||||
referenceSeriesUID: 'series-1',
|
||||
referencedImageId: '1.2.3.4',
|
||||
frameNumber: 1,
|
||||
metadata: { toolName: 'Length', referencedImageId: '1.2.3.4' },
|
||||
data: { cachedStats: {} },
|
||||
points: [
|
||||
[1, 2, 3],
|
||||
[4, 5, 6],
|
||||
],
|
||||
source: { name: 'cornerstone', version: '3d', uid: 'source-1' },
|
||||
isLocked: false,
|
||||
isVisible: true,
|
||||
displayText: { primary: ['1 mm'], secondary: [] },
|
||||
type: 'value_type::polyline',
|
||||
};
|
||||
|
||||
const annotation = {
|
||||
annotationUID: 'measurement-1',
|
||||
metadata: { toolName: 'Length', referencedImageId: '1.2.3.4' },
|
||||
data: {
|
||||
label: 'Length',
|
||||
handles: {
|
||||
points: [
|
||||
[1, 2, 3],
|
||||
[4, 5, 6],
|
||||
],
|
||||
},
|
||||
cachedStats: {},
|
||||
},
|
||||
};
|
||||
|
||||
mockAnnotations.set('measurement-1', annotation);
|
||||
|
||||
measurementService.emit(measurementService.EVENTS.MEASUREMENT_ADDED, {
|
||||
source: measurement.source,
|
||||
measurement,
|
||||
});
|
||||
|
||||
expect(backend.records).toHaveLength(1);
|
||||
expect(backend.records[0].studyInstanceUID).toBe('study-1');
|
||||
expect(backend.records[0].annotation.annotationUID).toBe('measurement-1');
|
||||
|
||||
backend.save({
|
||||
uid: 'measurement-1',
|
||||
studyInstanceUID: 'study-1',
|
||||
annotation,
|
||||
measurement,
|
||||
});
|
||||
backend.records = backend.records.filter(entry => entry.uid === 'measurement-1');
|
||||
await service.restorePersistedAnnotations('study-1');
|
||||
|
||||
expect(measurementService.addOrUpdateMeasurement).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ uid: 'measurement-1' }),
|
||||
expect.objectContaining({
|
||||
annotation: expect.objectContaining({ annotationUID: 'measurement-1' }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('uses the correct registered source on restore and skips re-saving', async () => {
|
||||
const backend = new FakeBackend();
|
||||
const measurementService = new FakeMeasurementService();
|
||||
const displaySetService = new FakeDisplaySetService();
|
||||
const service = new AnnotationPersistenceService(
|
||||
backend as any,
|
||||
{
|
||||
services: {
|
||||
measurementService: measurementService as any,
|
||||
displaySetService: displaySetService as any,
|
||||
},
|
||||
} as any
|
||||
);
|
||||
|
||||
const measurement = {
|
||||
uid: 'measurement-2',
|
||||
referenceStudyUID: 'study-2',
|
||||
source: { name: 'Cornerstone3DTools', version: '0.1' },
|
||||
};
|
||||
|
||||
backend.save({
|
||||
uid: 'measurement-2',
|
||||
studyInstanceUID: 'study-2',
|
||||
measurement,
|
||||
annotation: { annotationUID: 'measurement-2' },
|
||||
});
|
||||
|
||||
const saveSpy = jest.spyOn(backend, 'save');
|
||||
const updateSpy = jest.spyOn(backend, 'update');
|
||||
await service.restorePersistedAnnotations('study-2');
|
||||
|
||||
expect(measurementService.getSource).toHaveBeenCalledWith('Cornerstone3DTools', '0.1');
|
||||
expect(measurementService.addOrUpdateMeasurement).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
uid: 'measurement-2',
|
||||
source: expect.objectContaining({ name: 'Cornerstone3DTools', uid: 'test-source-uid' }),
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
|
||||
// Simulate MEASUREMENT_ADDED being fired by addOrUpdateMeasurement
|
||||
measurementService.emit(measurementService.EVENTS.MEASUREMENT_ADDED, {
|
||||
measurement: { uid: 'measurement-2', referenceStudyUID: 'study-2' },
|
||||
});
|
||||
|
||||
// Should not re-save during restore
|
||||
expect(saveSpy).not.toHaveBeenCalled();
|
||||
|
||||
// Simulate MEASUREMENT_UPDATED
|
||||
measurementService.emit(measurementService.EVENTS.MEASUREMENT_UPDATED, {
|
||||
measurement: { uid: 'measurement-2', referenceStudyUID: 'study-2' },
|
||||
});
|
||||
|
||||
// Second update should save/update
|
||||
measurementService.emit(measurementService.EVENTS.MEASUREMENT_UPDATED, {
|
||||
measurement: { uid: 'measurement-2', referenceStudyUID: 'study-2' },
|
||||
});
|
||||
|
||||
// Wait a tick for async updates to run
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(updateSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('removes measurement from backend when MEASUREMENT_REMOVED fires', async () => {
|
||||
const backend = new FakeBackend();
|
||||
const measurementService = new FakeMeasurementService();
|
||||
const service = new AnnotationPersistenceService(
|
||||
backend as any,
|
||||
{
|
||||
services: {
|
||||
measurementService: measurementService as any,
|
||||
},
|
||||
} as any
|
||||
);
|
||||
|
||||
backend.records.push({ uid: 'measurement-3', studyInstanceUID: 'study-3' });
|
||||
|
||||
measurementService.emit(measurementService.EVENTS.MEASUREMENT_REMOVED, {
|
||||
measurement: 'measurement-3',
|
||||
});
|
||||
|
||||
// Wait a tick for async delete to run
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(backend.records).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -0,0 +1,304 @@
|
||||
import { annotation } from '@cornerstonejs/tools';
|
||||
import LocalStorageAnnotationPersistenceBackend from './LocalStorageAnnotationPersistenceBackend';
|
||||
|
||||
export interface AnnotationPersistenceBackend {
|
||||
save(record: Record<string, any>): Promise<void> | void;
|
||||
update(record: Record<string, any>): Promise<void> | void;
|
||||
delete(uid: string): Promise<void> | void;
|
||||
load(studyInstanceUID: string): Promise<any[]> | any[];
|
||||
}
|
||||
|
||||
export interface AnnotationPersistenceServiceOptions {
|
||||
services: {
|
||||
measurementService: any;
|
||||
displaySetService?: any;
|
||||
};
|
||||
}
|
||||
|
||||
export type AnnotationPersistenceRecord = {
|
||||
uid: string;
|
||||
studyInstanceUID: string;
|
||||
seriesInstanceUID?: string;
|
||||
annotation: Record<string, any>;
|
||||
measurement: Record<string, any>;
|
||||
};
|
||||
|
||||
export default class AnnotationPersistenceService {
|
||||
public static REGISTRATION = {
|
||||
name: 'annotationPersistenceService',
|
||||
altName: 'AnnotationPersistenceService',
|
||||
create: ({ servicesManager }: { servicesManager: any }) => {
|
||||
const backend = new LocalStorageAnnotationPersistenceBackend();
|
||||
return new AnnotationPersistenceService(backend, {
|
||||
services: {
|
||||
measurementService: servicesManager.services.measurementService,
|
||||
displaySetService: servicesManager.services.displaySetService,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
private backend: AnnotationPersistenceBackend;
|
||||
private services: AnnotationPersistenceServiceOptions['services'];
|
||||
private currentStudyInstanceUID?: string;
|
||||
private restoredUIDs = new Set<string>();
|
||||
|
||||
constructor(backend: AnnotationPersistenceBackend, options: AnnotationPersistenceServiceOptions) {
|
||||
this.backend = backend;
|
||||
this.services = options.services;
|
||||
this._bindEvents();
|
||||
this._bindDisplaySetEvents();
|
||||
}
|
||||
|
||||
public async saveAnnotation(measurement: Record<string, any>): Promise<void> {
|
||||
if (!measurement?.uid || !measurement.referenceStudyUID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const record = this._buildRecord(measurement);
|
||||
try {
|
||||
await this.backend.save(record);
|
||||
} catch (error) {
|
||||
console.warn('Annotation persistence save failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
public async updateAnnotation(measurement: Record<string, any>): Promise<void> {
|
||||
if (!measurement?.uid || !measurement.referenceStudyUID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const record = this._buildRecord(measurement);
|
||||
try {
|
||||
await this.backend.update(record);
|
||||
} catch (error) {
|
||||
console.warn('Annotation persistence update failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteAnnotation(uid: string): Promise<void> {
|
||||
if (!uid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.backend.delete(uid);
|
||||
} catch (error) {
|
||||
console.warn('Annotation persistence delete failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
public async restorePersistedAnnotations(studyInstanceUID: string): Promise<void> {
|
||||
if (!studyInstanceUID) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentStudyInstanceUID = studyInstanceUID;
|
||||
try {
|
||||
const records = await this.backend.load(studyInstanceUID);
|
||||
for (const record of records) {
|
||||
if (!record?.measurement?.uid || this.restoredUIDs.has(record.measurement.uid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.restoredUIDs.add(record.measurement.uid);
|
||||
this._restoreMeasurement(record);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Annotation persistence restore failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private _bindEvents(): void {
|
||||
const measurementService = this.services.measurementService;
|
||||
if (!measurementService) {
|
||||
return;
|
||||
}
|
||||
|
||||
measurementService.subscribe(measurementService.EVENTS.MEASUREMENT_ADDED, ({ measurement }) => {
|
||||
if (this.restoredUIDs.has(measurement.uid)) {
|
||||
return;
|
||||
}
|
||||
this.saveAnnotation(measurement).catch(() => undefined);
|
||||
});
|
||||
|
||||
measurementService.subscribe(
|
||||
measurementService.EVENTS.MEASUREMENT_UPDATED,
|
||||
({ measurement }) => {
|
||||
if (this.restoredUIDs.has(measurement.uid)) {
|
||||
this.restoredUIDs.delete(measurement.uid);
|
||||
return;
|
||||
}
|
||||
this.updateAnnotation(measurement).catch(() => undefined);
|
||||
}
|
||||
);
|
||||
|
||||
measurementService.subscribe(
|
||||
measurementService.EVENTS.MEASUREMENT_REMOVED,
|
||||
({ measurement }) => {
|
||||
this.deleteAnnotation(measurement).catch(() => undefined);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _bindDisplaySetEvents(): void {
|
||||
const displaySetService = this.services.displaySetService;
|
||||
if (!displaySetService?.subscribe) {
|
||||
return;
|
||||
}
|
||||
|
||||
displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
||||
({ displaySetsAdded }) => {
|
||||
const studyInstanceUIDs = displaySetsAdded?.map(displaySet => displaySet?.StudyInstanceUID);
|
||||
studyInstanceUIDs?.forEach(studyInstanceUID => {
|
||||
if (studyInstanceUID) {
|
||||
this.restorePersistedAnnotations(studyInstanceUID).catch(() => undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _buildRecord(measurement: Record<string, any>): AnnotationPersistenceRecord {
|
||||
const annotationManager = annotation.state.getAnnotationManager();
|
||||
const annotationInstance = annotation.state.getAnnotation(measurement.uid);
|
||||
const studyInstanceUID = measurement.referenceStudyUID;
|
||||
const seriesInstanceUID = measurement.referenceSeriesUID;
|
||||
|
||||
const annotationPayload = annotationInstance || {
|
||||
annotationUID: measurement.uid,
|
||||
metadata: {
|
||||
...measurement.metadata,
|
||||
toolName: measurement.toolName,
|
||||
},
|
||||
data: {
|
||||
label: measurement.label,
|
||||
handles: measurement.points ? { points: measurement.points } : undefined,
|
||||
cachedStats: measurement.data || {},
|
||||
},
|
||||
};
|
||||
|
||||
const record = {
|
||||
uid: measurement.uid,
|
||||
studyInstanceUID,
|
||||
seriesInstanceUID,
|
||||
annotation: annotationPayload,
|
||||
measurement: {
|
||||
...measurement,
|
||||
annotationUID: measurement.uid,
|
||||
metadata: {
|
||||
...(measurement.metadata || {}),
|
||||
toolName: measurement.toolName,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
private _restoreMeasurement(record: AnnotationPersistenceRecord): void {
|
||||
const measurementService = this.services.measurementService;
|
||||
if (!measurementService) {
|
||||
return;
|
||||
}
|
||||
|
||||
const persistedSource = record.measurement.source;
|
||||
const resolvedSource =
|
||||
(persistedSource?.name && persistedSource?.version
|
||||
? measurementService.getSource(persistedSource.name, persistedSource.version)
|
||||
: null) ||
|
||||
measurementService.getSource('Cornerstone3DTools', '0.1');
|
||||
|
||||
if (!resolvedSource) {
|
||||
console.warn('Annotation persistence: no registered source found, skipping restore');
|
||||
return;
|
||||
}
|
||||
|
||||
const measurement = {
|
||||
...record.measurement,
|
||||
uid: record.measurement.uid || record.uid,
|
||||
referenceStudyUID: record.studyInstanceUID,
|
||||
referenceSeriesUID: record.seriesInstanceUID,
|
||||
source: resolvedSource,
|
||||
};
|
||||
|
||||
if (
|
||||
this.currentStudyInstanceUID &&
|
||||
measurement.referenceStudyUID !== this.currentStudyInstanceUID
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedAnnotation = this._normalizePersistedAnnotation(record.annotation);
|
||||
const rebuiltAnnotation = this._buildRestoredAnnotation(measurement, normalizedAnnotation);
|
||||
|
||||
measurementService.addOrUpdateMeasurement?.(measurement, {
|
||||
annotation: rebuiltAnnotation,
|
||||
});
|
||||
}
|
||||
|
||||
private _normalizePersistedAnnotation(
|
||||
annotationPayload?: Record<string, any>
|
||||
): Record<string, any> {
|
||||
if (!annotationPayload) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const normalizedData = annotationPayload.data || {};
|
||||
const normalizedHandles = normalizedData.handles || {};
|
||||
const rawPoints = Array.isArray(normalizedHandles.points)
|
||||
? normalizedHandles.points
|
||||
: Array.isArray(normalizedData.points)
|
||||
? normalizedData.points
|
||||
: Array.isArray(annotationPayload.points)
|
||||
? annotationPayload.points
|
||||
: [];
|
||||
const normalizedPoints = rawPoints.filter(point => Array.isArray(point) && point.length >= 2);
|
||||
|
||||
return {
|
||||
...annotationPayload,
|
||||
data: {
|
||||
...normalizedData,
|
||||
handles: {
|
||||
...normalizedHandles,
|
||||
points: normalizedPoints,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private _buildRestoredAnnotation(
|
||||
measurement: Record<string, any>,
|
||||
annotationPayload?: Record<string, any>
|
||||
): Record<string, any> {
|
||||
const normalizedAnnotation = this._normalizePersistedAnnotation(annotationPayload);
|
||||
const persistedData = normalizedAnnotation.data || {};
|
||||
const persistedHandles = persistedData.handles || {};
|
||||
const points =
|
||||
Array.isArray(persistedHandles.points) && persistedHandles.points.length
|
||||
? persistedHandles.points
|
||||
: Array.isArray(measurement.points)
|
||||
? measurement.points.filter(point => Array.isArray(point) && point.length >= 2)
|
||||
: [];
|
||||
|
||||
return {
|
||||
annotationUID: measurement.uid || normalizedAnnotation.annotationUID,
|
||||
metadata: {
|
||||
...(normalizedAnnotation.metadata || {}),
|
||||
toolName: measurement.toolName || normalizedAnnotation.metadata?.toolName,
|
||||
FrameOfReferenceUID: measurement.FrameOfReferenceUID,
|
||||
referencedImageId: measurement.referencedImageId,
|
||||
},
|
||||
data: {
|
||||
...(persistedData || {}),
|
||||
label: persistedData.label || measurement.label,
|
||||
handles: {
|
||||
...persistedHandles,
|
||||
points,
|
||||
},
|
||||
cachedStats: { ...(persistedData.cachedStats || measurement.data || {}) },
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
export interface AnnotationPersistenceBackend {
|
||||
save(record: Record<string, any>): Promise<void> | void;
|
||||
update(record: Record<string, any>): Promise<void> | void;
|
||||
delete(uid: string): Promise<void> | void;
|
||||
load(studyInstanceUID: string): Promise<any[]> | any[];
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'ohif.annotation.persistence';
|
||||
|
||||
export default class LocalStorageAnnotationPersistenceBackend
|
||||
implements AnnotationPersistenceBackend
|
||||
{
|
||||
public save(record: Record<string, any>): void {
|
||||
const entries = this._readEntries();
|
||||
const nextEntries = entries.filter(existing => existing.uid !== record.uid);
|
||||
nextEntries.push(record);
|
||||
this._writeEntries(nextEntries);
|
||||
}
|
||||
|
||||
public update(record: Record<string, any>): void {
|
||||
this.save(record);
|
||||
}
|
||||
|
||||
public delete(uid: string): void {
|
||||
const entries = this._readEntries().filter(existing => existing.uid !== uid);
|
||||
this._writeEntries(entries);
|
||||
}
|
||||
|
||||
public load(studyInstanceUID: string): any[] {
|
||||
return this._readEntries().filter(entry => entry.studyInstanceUID === studyInstanceUID);
|
||||
}
|
||||
|
||||
private _readEntries(): Record<string, any>[] {
|
||||
if (typeof window === 'undefined' || !window.localStorage) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (error) {
|
||||
console.warn('Failed to read persisted annotations from storage:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private _writeEntries(entries: Record<string, any>[]): void {
|
||||
if (typeof window === 'undefined' || !window.localStorage) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
import AnnotationPersistenceService from './AnnotationPersistenceService';
|
||||
import type {
|
||||
AnnotationPersistenceBackend,
|
||||
AnnotationPersistenceRecord,
|
||||
AnnotationPersistenceServiceOptions,
|
||||
} from './AnnotationPersistenceService';
|
||||
import LocalStorageAnnotationPersistenceBackend from './LocalStorageAnnotationPersistenceBackend';
|
||||
|
||||
export type {
|
||||
AnnotationPersistenceBackend,
|
||||
AnnotationPersistenceRecord,
|
||||
AnnotationPersistenceServiceOptions,
|
||||
};
|
||||
export { LocalStorageAnnotationPersistenceBackend };
|
||||
|
||||
export default AnnotationPersistenceService;
|
||||
@ -0,0 +1,20 @@
|
||||
import AnnotationPersistenceService, {
|
||||
AnnotationPersistenceServiceOptions,
|
||||
} from './AnnotationPersistenceService';
|
||||
import LocalStorageAnnotationPersistenceBackend from './LocalStorageAnnotationPersistenceBackend';
|
||||
|
||||
const registration = {
|
||||
name: 'annotationPersistenceService',
|
||||
altName: 'AnnotationPersistenceService',
|
||||
create: ({ servicesManager }: { servicesManager: any }) => {
|
||||
const backend = new LocalStorageAnnotationPersistenceBackend();
|
||||
return new AnnotationPersistenceService(backend, {
|
||||
services: {
|
||||
measurementService: servicesManager.services.measurementService,
|
||||
displaySetService: servicesManager.services.displaySetService,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default registration;
|
||||
@ -385,6 +385,45 @@ class MeasurementService extends PubSubService {
|
||||
return updatedMeasurement.uid;
|
||||
}
|
||||
|
||||
public addOrUpdateMeasurement(measurement, restoreData?: any) {
|
||||
if (!measurement?.uid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingMeasurement = this.measurements.get(measurement.uid);
|
||||
const restoredMeasurement = {
|
||||
...(existingMeasurement || {}),
|
||||
...measurement,
|
||||
modifiedTimestamp: Math.floor(Date.now() / 1000),
|
||||
uid: measurement.uid,
|
||||
};
|
||||
|
||||
this.measurements.set(restoredMeasurement.uid, restoredMeasurement);
|
||||
|
||||
if (existingMeasurement) {
|
||||
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
|
||||
source: restoredMeasurement.source,
|
||||
measurement: restoredMeasurement,
|
||||
notYetUpdatedAtSource: false,
|
||||
});
|
||||
return restoredMeasurement.uid;
|
||||
}
|
||||
|
||||
this._broadcastEvent(this.EVENTS.MEASUREMENT_ADDED, {
|
||||
source: restoredMeasurement.source,
|
||||
measurement: restoredMeasurement,
|
||||
});
|
||||
|
||||
this._broadcastEvent(this.EVENTS.RAW_MEASUREMENT_ADDED, {
|
||||
source: restoredMeasurement.source,
|
||||
measurement: restoredMeasurement,
|
||||
data: restoreData || {},
|
||||
dataSource: {},
|
||||
});
|
||||
|
||||
return restoredMeasurement.uid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a raw measurement into a source so that it may be
|
||||
* Converted to/from annotation in the same way. E.g. import serialized data
|
||||
|
||||
@ -18,6 +18,7 @@ import PanelService from './PanelService';
|
||||
import WorkflowStepsService from './WorkflowStepsService';
|
||||
import StudyPrefetcherService from './StudyPrefetcherService';
|
||||
import { MultiMonitorService } from './MultiMonitorService';
|
||||
import AnnotationPersistenceService from './AnnotationPersistenceService';
|
||||
|
||||
import type Services from '../types/Services';
|
||||
|
||||
@ -46,4 +47,5 @@ export {
|
||||
PanelService,
|
||||
WorkflowStepsService,
|
||||
StudyPrefetcherService,
|
||||
AnnotationPersistenceService,
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ohif-docs",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/i18n",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "Internationalization library for The OHIF Viewer",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/ui-next",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "Next version of OHIF Viewers UI, more customizable using shadcn/ui",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ShepherdBase } from 'shepherd.js';
|
||||
import { offset, flip, shift, detectOverflow } from '@floating-ui/dom';
|
||||
import { offset, flip, shift } from '@floating-ui/dom';
|
||||
|
||||
/**
|
||||
* Retrieves the list of tours that have been shown from localStorage.
|
||||
@ -51,41 +51,13 @@ const defaultShowHandler = (Shepherd: ShepherdBase) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom middleware for adjusting Shepherd step positioning when overflowing.
|
||||
*
|
||||
* @type {object}
|
||||
* @property {string} name - The name of the middleware.
|
||||
* @property {function} fn - The function that adjusts the position of the step when overflowing.
|
||||
*/
|
||||
|
||||
const customMiddleware = {
|
||||
name: 'customOverflowMiddleware',
|
||||
async fn(state) {
|
||||
const overflow = await detectOverflow(state, {
|
||||
boundary: document.querySelector('body'),
|
||||
padding: 24,
|
||||
});
|
||||
|
||||
const xAdjustment =
|
||||
overflow.left > 0 ? overflow.left : overflow.right > 0 ? -overflow.right : 0;
|
||||
const yAdjustment =
|
||||
overflow.top > 0 ? overflow.top : overflow.bottom > 0 ? -overflow.bottom : 0;
|
||||
|
||||
return {
|
||||
x: state.x + xAdjustment,
|
||||
y: state.y + yAdjustment,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Default Floating UI middleware for positioning steps in Shepherd.js.
|
||||
* Includes offset, shift, flip, and custom overflow middleware.
|
||||
* Includes offset, shift, and flip middleware.
|
||||
*
|
||||
* @type {Array<object>}
|
||||
*/
|
||||
|
||||
const middleware = [offset(15), shift(), flip(), customMiddleware];
|
||||
const middleware = [offset(15), shift({ padding: 24 }), flip()];
|
||||
|
||||
export { hasTourBeenShown, markTourAsShown, middleware, defaultShowHandler };
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/ui",
|
||||
"version": "3.13.0-beta.124",
|
||||
"version": "3.13.0-beta.125",
|
||||
"description": "A set of React components for Medical Imaging Viewers",
|
||||
"author": "OHIF Contributors",
|
||||
"license": "MIT",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user