@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 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.
@@ -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,3 @@
1
+ import React from "react";
2
+ export declare const DicomPlaybackControls: React.FC;
3
+ export default DicomPlaybackControls;
@@ -0,0 +1,2 @@
1
+ import React from "react";
2
+ export declare const DicomToolbar: React.FC;
@@ -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,2 @@
1
+ import React from "react";
2
+ export declare const DicomViewport: React.FC;
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ declare const MobileTimelineSlider: React.FC;
3
+ export default MobileTimelineSlider;
@@ -0,0 +1,2 @@
1
+ import React from "react";
2
+ export declare const MobileToolsPanel: React.FC;
@@ -0,0 +1 @@
1
+ export declare const PresetsDropdown: () => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,2 @@
1
+ declare const Footer: () => import("react/jsx-runtime").JSX.Element;
2
+ export default Footer;
@@ -0,0 +1,2 @@
1
+ declare const SideBar: () => import("react/jsx-runtime").JSX.Element;
2
+ export default SideBar;
@@ -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
+ };
@@ -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";