@medicaresoft/dicom-viewer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -0
- package/dist/Action.d.ts +8 -0
- package/dist/DicomViewer.d.ts +30 -0
- package/dist/components/DicomOverlays.d.ts +10 -0
- package/dist/components/DicomPlaybackControls.d.ts +3 -0
- package/dist/components/DicomToolbar.d.ts +2 -0
- package/dist/components/DicomToolbarButton.d.ts +14 -0
- package/dist/components/DicomViewport.d.ts +2 -0
- package/dist/components/MobileTimelineSlider.d.ts +3 -0
- package/dist/components/MobileToolsPanel.d.ts +2 -0
- package/dist/components/PresetsDropdown.d.ts +1 -0
- package/dist/components/footer.d.ts +2 -0
- package/dist/components/sidebar.d.ts +2 -0
- package/dist/constants.d.ts +8 -0
- package/dist/context.d.ts +12 -0
- package/dist/downloadQueue.d.ts +12 -0
- package/dist/hooks/useCornerstoneViewer.d.ts +27 -0
- package/dist/hooks/useDicomViewerState.d.ts +15 -0
- package/dist/hooks/useDragScroll.d.ts +11 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +36449 -0
- package/dist/interfaces.d.ts +117 -0
- package/dist/skeleton.d.ts +3 -0
- package/dist/skeleton.js +136 -0
- package/dist/style.css +3 -0
- package/dist/types.d.ts +85 -0
- package/dist/ui/Skeleton.d.ts +1 -0
- package/dist/ui/Tabs.d.ts +20 -0
- package/dist/ui/cn.d.ts +2 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# @medicaresoft/dicom-viewer
|
|
2
|
+
|
|
3
|
+
Visor DICOM reutilizable para React, basado en Cornerstone v2
|
|
4
|
+
(core/tools/wado-image-loader). Es el **núcleo de visualización** de un PACS:
|
|
5
|
+
sin lógica de negocio, sin permisos, sin router y sin HTTP propio. Todo lo
|
|
6
|
+
específico del host se inyecta por props.
|
|
7
|
+
|
|
8
|
+
## Instalación
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @medicaresoft/dicom-viewer
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Peer dependencies: `react` y `react-dom` (>= 18).
|
|
15
|
+
|
|
16
|
+
## Uso mínimo
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import { DicomViewer } from "@medicaresoft/dicom-viewer";
|
|
20
|
+
import "@medicaresoft/dicom-viewer/style.css"; // si tu host NO compila Tailwind sobre el paquete
|
|
21
|
+
|
|
22
|
+
export function App() {
|
|
23
|
+
return (
|
|
24
|
+
<DicomViewer
|
|
25
|
+
study={study} // metadatos (ver DicomStudy)
|
|
26
|
+
getDicomFile={(uid) => fetchMyDicom(uid)} // (instanceUid) => Promise<Blob>
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
El host decide de dónde salen los bytes: API propia, DICOMweb, disco local
|
|
33
|
+
(Wails), etc. El visor solo pide `(instanceUid) => Blob`.
|
|
34
|
+
|
|
35
|
+
## Props principales
|
|
36
|
+
|
|
37
|
+
| Prop | Tipo | Descripción |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `study` | `DicomStudy` | Metadatos del estudio (paciente + series + files con `instanceUid`) |
|
|
40
|
+
| `getDicomFile` | `(instanceUid) => Promise<Blob \| ArrayBuffer>` | Fuente de bytes DICOM (requerido) |
|
|
41
|
+
| `getThumbnailUrl` | `(instanceUid) => string` | Miniaturas de series (opcional) |
|
|
42
|
+
| `presets` | `Record<string, Record<string, { W; C }>>` | Presets de ventana por modalidad (opcional) |
|
|
43
|
+
| `maxConcurrentDownloads` | `number` | Límite global de descargas concurrentes (default 12) |
|
|
44
|
+
| `branding` | `{ logo?, name? }` | Logo/nombre en overlays |
|
|
45
|
+
| `onBack` | `() => void` | Si se define, muestra botón volver |
|
|
46
|
+
| `actions` | `ReactNode` | `<Action/>` inyectadas en slots |
|
|
47
|
+
| `modals` | `ReactNode` | `<ViewerModal/>` inyectados |
|
|
48
|
+
|
|
49
|
+
## Acciones y modales (slots)
|
|
50
|
+
|
|
51
|
+
El visor reserva zonas (toolbar/sidebar/footer) donde el host inyecta sus
|
|
52
|
+
botones; los permisos viven en el host:
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { DicomViewer, Action, ViewerModal } from "@medicaresoft/dicom-viewer";
|
|
56
|
+
|
|
57
|
+
<DicomViewer study={study} getDicomFile={fetcher}>
|
|
58
|
+
<Action
|
|
59
|
+
placement="sidebar"
|
|
60
|
+
id="reports"
|
|
61
|
+
label="Informes"
|
|
62
|
+
icon={<FileText size={16} />}
|
|
63
|
+
visible={() => user.can("reports.view")} // permisos del host
|
|
64
|
+
onClick={(ctx) => setShowReports(true)} // ctx: { study, activeSeriesIndex }
|
|
65
|
+
/>
|
|
66
|
+
<ViewerModal id="reports" open={showReports}>
|
|
67
|
+
<MyReportsModal onClose={() => setShowReports(false)} />
|
|
68
|
+
</ViewerModal>
|
|
69
|
+
</DicomViewer>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`actions` y `modals` también aceptan arrays de `<Action/>`/`<ViewerModal/>`.
|
|
73
|
+
Si una acción trae `children`, se renderiza tal cual (botón 100% custom del
|
|
74
|
+
host).
|
|
75
|
+
|
|
76
|
+
## Skeleton ligero
|
|
77
|
+
|
|
78
|
+
Subpath sin Cornerstone para usar como fallback mientras se carga el visor:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { lazy, Suspense } from "react";
|
|
82
|
+
import { DicomViewerSkeleton } from "@medicaresoft/dicom-viewer/skeleton";
|
|
83
|
+
|
|
84
|
+
const DicomViewer = lazy(() =>
|
|
85
|
+
import("@medicaresoft/dicom-viewer").then((m) => ({ default: m.DicomViewer })),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
<Suspense fallback={<DicomViewerSkeleton />}>
|
|
89
|
+
<DicomViewer study={study} getDicomFile={fetcher} />
|
|
90
|
+
</Suspense>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Estilos
|
|
94
|
+
|
|
95
|
+
- **Host con Tailwind v4**: agrega el paquete a tu contenido
|
|
96
|
+
(`@source "../node_modules/@medicaresoft/dicom-viewer/dist";`) y sobreescribe
|
|
97
|
+
las variables `--dv-primary` / `--dv-secondary` para tu marca.
|
|
98
|
+
- **Host sin Tailwind**: importa `@medicaresoft/dicom-viewer/style.css`.
|
|
99
|
+
|
|
100
|
+
## Tipos
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
interface DicomStudy {
|
|
104
|
+
id: string;
|
|
105
|
+
studyInstanceUid: string;
|
|
106
|
+
patientName: string;
|
|
107
|
+
patientId: string;
|
|
108
|
+
patientBirthDate?: string;
|
|
109
|
+
patientAge?: string;
|
|
110
|
+
patientSex?: string;
|
|
111
|
+
studyDate?: string;
|
|
112
|
+
studyTime?: string;
|
|
113
|
+
studyDescription?: string;
|
|
114
|
+
series: Array<{
|
|
115
|
+
id: string;
|
|
116
|
+
seriesInstanceUid: string;
|
|
117
|
+
modality: string;
|
|
118
|
+
name: string;
|
|
119
|
+
files: Array<{ position: number; instanceUid: string }>;
|
|
120
|
+
}>;
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
El objeto de estudio de tu app es compatible por asignación estructural
|
|
125
|
+
(campos extra permitidos).
|
|
126
|
+
|
|
127
|
+
## Notas
|
|
128
|
+
|
|
129
|
+
- Basado en la generación legacy de Cornerstone (v2); migración a
|
|
130
|
+
Cornerstone3D prevista como fase posterior, sin cambios de API.
|
|
131
|
+
- El visor asigna dependencias a `window` (cornerstone, hammerjs) al
|
|
132
|
+
inicializarse — requerido por las libs legacy.
|
package/dist/Action.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ActionProps, ViewerModalProps } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Componentes declarativos para inyectar botones y modales.
|
|
4
|
+
* No pintan nada por sí mismos: `DicomViewer` lee sus props y los reparte a
|
|
5
|
+
* los slots reservados (toolbar/sidebar/footer) y a la capa de modales.
|
|
6
|
+
*/
|
|
7
|
+
export declare const Action: (_props: ActionProps) => null;
|
|
8
|
+
export declare const ViewerModal: ({ open, children }: ViewerModalProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { DicomFileFetcher, DicomStudy, PresetsConfig, ThumbnailUrlResolver, ViewerBranding } from "./types";
|
|
3
|
+
export interface DicomViewerProps {
|
|
4
|
+
/** Metadatos del estudio (el host los obtiene de donde quiera). */
|
|
5
|
+
study: DicomStudy;
|
|
6
|
+
/** Fuente de bytes DICOM inyectada por el host: (instanceUid) => Blob. */
|
|
7
|
+
getDicomFile: DicomFileFetcher;
|
|
8
|
+
/** Resuelve URLs de miniaturas de series (opcional; sin esto no hay thumbs). */
|
|
9
|
+
getThumbnailUrl?: ThumbnailUrlResolver;
|
|
10
|
+
/** Presets de ventana por modalidad (opcional). */
|
|
11
|
+
presets?: PresetsConfig;
|
|
12
|
+
/** Máximo global de descargas concurrentes entre todos los viewports. */
|
|
13
|
+
maxConcurrentDownloads?: number;
|
|
14
|
+
/** Logo y nombre de institución para overlays/toolbar (opcional). */
|
|
15
|
+
branding?: ViewerBranding;
|
|
16
|
+
/** Si se define, se muestra el botón de volver; el paquete no usa router. */
|
|
17
|
+
onBack?: () => void;
|
|
18
|
+
/** Acciones inyectadas: <Action placement=... /> para toolbar/sidebar/footer. */
|
|
19
|
+
actions?: ReactNode;
|
|
20
|
+
/** Modales inyectados: <ViewerModal open=...>...</ViewerModal>. */
|
|
21
|
+
modals?: ReactNode;
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Visor DICOM reutilizable. Núcleo de visualización: sin permisos, sin
|
|
26
|
+
* lógica de negocio, sin router y sin HTTP propio. Todo lo específico del
|
|
27
|
+
* host llega por props (datos, fetchers, branding, acciones y modales).
|
|
28
|
+
*/
|
|
29
|
+
export declare const DicomViewer: ({ study, getDicomFile, getThumbnailUrl, presets, maxConcurrentDownloads, branding, onBack, actions: actionsProp, modals: modalsProp, className, }: DicomViewerProps) => import("react/jsx-runtime").JSX.Element;
|
|
30
|
+
export default DicomViewer;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
interface DicomOverlaysProps {
|
|
3
|
+
seriesIndex?: number;
|
|
4
|
+
currentImageIndex?: number;
|
|
5
|
+
totalImages?: number;
|
|
6
|
+
ww?: number | null;
|
|
7
|
+
wc?: number | null;
|
|
8
|
+
}
|
|
9
|
+
export declare const DicomOverlays: React.FC<DicomOverlaysProps>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
interface DicomToolbarButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
3
|
+
active?: boolean;
|
|
4
|
+
variant?: "tool" | "action" | "destructive";
|
|
5
|
+
icon?: React.ReactNode;
|
|
6
|
+
label?: string;
|
|
7
|
+
boundButtons?: {
|
|
8
|
+
left?: boolean;
|
|
9
|
+
right?: boolean;
|
|
10
|
+
middle?: boolean;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export declare const DicomToolbarButton: React.FC<DicomToolbarButtonProps>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const PresetsDropdown: () => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type LucideIcon } from "lucide-react";
|
|
2
|
+
export interface ToolButtonConfig {
|
|
3
|
+
id: "Wwwc" | "Zoom" | "Pan" | "Length" | "Angle" | "CobbAngle" | "Probe" | "EllipticalRoi" | "RectangleRoi" | "Magnify";
|
|
4
|
+
label: string;
|
|
5
|
+
Icon: LucideIcon;
|
|
6
|
+
title: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const toolButtons: readonly ToolButtonConfig[];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DicomStudy, ActionPlacement, ActionProps } from "./types";
|
|
2
|
+
import type { DicomContextType, DicomViewerAction, DicomViewerState } from "./interfaces";
|
|
3
|
+
export declare const DicomViewerContext: import("react").Context<DicomContextType | undefined>;
|
|
4
|
+
export declare const useDicomContext: () => DicomContextType;
|
|
5
|
+
export declare const initialDicomState: (study: DicomStudy) => DicomViewerState;
|
|
6
|
+
export declare function dicomViewerReducer(state: DicomViewerState, action: DicomViewerAction): DicomViewerState;
|
|
7
|
+
/**
|
|
8
|
+
* Devuelve las acciones inyectadas para un slot, ya filtradas por `visible()`.
|
|
9
|
+
* La evaluación ocurre en cada render del slot (los permisos del host viven
|
|
10
|
+
* en `visible` y se re-evalúan cuando el wrapper re-renderiza).
|
|
11
|
+
*/
|
|
12
|
+
export declare const useViewerActions: (placement: ActionPlacement) => ActionProps[];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cola global de descargas compartida entre todos los viewports del visor.
|
|
3
|
+
* Cada viewport encola la descarga de sus instancias; la cola limita el total
|
|
4
|
+
* de peticiones concurrentes del visor completo (evita 4×8=32 con layout 2x2).
|
|
5
|
+
*/
|
|
6
|
+
export declare class DownloadQueue {
|
|
7
|
+
private readonly maxConcurrent;
|
|
8
|
+
private active;
|
|
9
|
+
private waiting;
|
|
10
|
+
constructor(maxConcurrent: number);
|
|
11
|
+
run<T>(task: () => Promise<T>): Promise<T>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DicomFileFetcher, DicomStudy } from "../types";
|
|
2
|
+
import type { DownloadQueue } from "../downloadQueue";
|
|
3
|
+
export declare function useCornerstoneViewer(study: DicomStudy, seriesIndex?: number, onImageIndexChange?: (index: number) => void, playSpeed?: number, showReferenceLines?: boolean, referenceLinesSynchronizer?: any, getDicomFile?: DicomFileFetcher, downloadQueue?: DownloadQueue): {
|
|
4
|
+
viewportRef: import("react").RefObject<HTMLDivElement | null>;
|
|
5
|
+
isLoading: boolean;
|
|
6
|
+
loadingProgress: number;
|
|
7
|
+
downloadedCount: number;
|
|
8
|
+
totalImages: number;
|
|
9
|
+
currentImageIndex: number;
|
|
10
|
+
activeTool: string;
|
|
11
|
+
toolBindings: Record<number, string>;
|
|
12
|
+
isPlaying: boolean;
|
|
13
|
+
ww: number | null;
|
|
14
|
+
wc: number | null;
|
|
15
|
+
zoomScale: number;
|
|
16
|
+
changeTool: (toolName: string, mouseButtonMask?: number) => void;
|
|
17
|
+
handleSliderChange: (index: number) => Promise<void>;
|
|
18
|
+
togglePlay: () => void;
|
|
19
|
+
handleResetView: () => void;
|
|
20
|
+
handleClearMeasurements: () => void;
|
|
21
|
+
setWindowing: (width: number, center: number) => void;
|
|
22
|
+
handleInvert: () => void;
|
|
23
|
+
handleRotateCw: () => void;
|
|
24
|
+
handleRotateCcw: () => void;
|
|
25
|
+
handleFlipHorizontal: () => void;
|
|
26
|
+
handleFlipVertical: () => void;
|
|
27
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ReactElement } from "react";
|
|
2
|
+
import type { DicomContextType } from "../interfaces";
|
|
3
|
+
import type { ActionProps, DicomFileFetcher, DicomStudy, PresetsConfig, ThumbnailUrlResolver, ViewerBranding, ViewerModalProps } from "../types";
|
|
4
|
+
/** Configuración inyectada por el host al componente DicomViewer. */
|
|
5
|
+
export interface ViewerSetupConfig {
|
|
6
|
+
getDicomFile: DicomFileFetcher;
|
|
7
|
+
getThumbnailUrl?: ThumbnailUrlResolver;
|
|
8
|
+
presets?: PresetsConfig;
|
|
9
|
+
maxConcurrentDownloads?: number;
|
|
10
|
+
branding?: ViewerBranding;
|
|
11
|
+
onBack?: () => void;
|
|
12
|
+
actions?: ReactElement<ActionProps>[];
|
|
13
|
+
modals?: ReactElement<ViewerModalProps>[];
|
|
14
|
+
}
|
|
15
|
+
export declare const useDicomViewerState: (study: DicomStudy, config: ViewerSetupConfig) => DicomContextType;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drag-scroll horizontal para contenedores overflow (toolbar de herramientas
|
|
3
|
+
* y lista de series). Utilidad genérica sin lógica de negocio.
|
|
4
|
+
*/
|
|
5
|
+
export declare function useDragScroll(): {
|
|
6
|
+
onMouseDown: (e: React.MouseEvent<HTMLDivElement>) => void;
|
|
7
|
+
onMouseLeave: () => void;
|
|
8
|
+
onMouseUp: () => void;
|
|
9
|
+
onMouseMove: (e: React.MouseEvent<HTMLDivElement>) => void;
|
|
10
|
+
onClickCapture: (e: React.MouseEvent<HTMLDivElement>) => void;
|
|
11
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import "./style.css";
|
|
2
|
+
export { DicomViewer } from "./DicomViewer";
|
|
3
|
+
export type { DicomViewerProps } from "./DicomViewer";
|
|
4
|
+
export { Action, ViewerModal } from "./Action";
|
|
5
|
+
export { DicomToolbarButton } from "./components/DicomToolbarButton";
|
|
6
|
+
export { useDicomContext, useViewerActions, DicomViewerContext } from "./context";
|
|
7
|
+
export type { ActionContext, ActionPlacement, ActionProps, DicomFile, DicomFileFetcher, DicomSeries, DicomStudy, PresetsConfig, ThumbnailUrlResolver, ViewerBranding, ViewerModalProps, } from "./types";
|